mirror of
https://github.com/progval/irctest.git
synced 2025-04-04 22:39:50 +00:00
Compare commits
142 Commits
sopel-scra
...
e9e37f5438
Author | SHA1 | Date | |
---|---|---|---|
e9e37f5438 | |||
2b6b666426 | |||
d218d2f98d | |||
52178a99e5 | |||
7f2a631a1a | |||
cd9f801b92 | |||
ccd647ea61 | |||
9de64159ba | |||
638f959c95 | |||
06e08b52be | |||
54a1ab95ce | |||
3e6d97ae42 | |||
00c130d66c | |||
2680502dfe | |||
d090f5455e | |||
c31aaf4d61 | |||
7b0ee7589f | |||
d202e440bb | |||
03ad671951 | |||
e3485b92b4 | |||
75d9040d37 | |||
a132440789 | |||
aaa2e26b6e | |||
052198c61b | |||
9f33633cc7 | |||
465f6637ed | |||
9856317a64 | |||
af980ed3b6 | |||
15c077d511 | |||
330300eba1 | |||
f265e28702 | |||
e3ffff6ad4 | |||
f4806dcb2b | |||
395482f7b5 | |||
2dea91e17a | |||
df626de5ed | |||
79223d35f1 | |||
723991c7ec | |||
1bc8741479 | |||
9f8e712776 | |||
a1f8fcac49 | |||
d3c919e0f5 | |||
ce51dddc15 | |||
7f9b4b315f | |||
9d43a002c2 | |||
ea66a8f9a4 | |||
473db1cc5b | |||
f4a01cfe49 | |||
e6dfb87759 | |||
2ae612c68f | |||
d908699674 | |||
61ae4bcf9e | |||
0c5c91368a | |||
c0e6ca4dde | |||
e6d54db9ce | |||
03b6fbbfc2 | |||
ee6c56d84b | |||
85b519d93a | |||
56e0565512 | |||
df2880e379 | |||
61a6f047d2 | |||
d75e3fae34 | |||
0ebfbdf6ab | |||
0f6a485d7d | |||
dfd429014a | |||
d9ad638791 | |||
246a259111 | |||
18d04e8f80 | |||
6425e707ac | |||
032d0e32de | |||
62a039498b | |||
1a48ddb498 | |||
17c7ccede9 | |||
1548287335 | |||
4f1a84b5a8 | |||
d88349a403 | |||
2ee8a0694f | |||
81094a308b | |||
edf82585af | |||
00663f15ec | |||
36901c1433 | |||
558add5229 | |||
805635c839 | |||
e1ff9fd7fe | |||
c3aa97c428 | |||
3692f2d79d | |||
04d0c8000f | |||
ecc560adeb | |||
c58167b42d | |||
34c78e5d2f | |||
1c6a7188d6 | |||
50d3a8e6da | |||
fe24e4b8b8 | |||
360a853bca | |||
653d818421 | |||
10e07aa800 | |||
b28820e562 | |||
cb147f46eb | |||
a950c724bb | |||
7255d65514 | |||
61fb287280 | |||
d190a91960 | |||
59b2cd729b | |||
e38f29befa | |||
2e45f7bfdb | |||
7bc8a81f8a | |||
4ee9c9c53a | |||
321e254d15 | |||
e5f22e8080 | |||
5a5dbdb50d | |||
52c22236a6 | |||
22c6743b24 | |||
b04db62a9b | |||
5ec44e1417 | |||
2fb8ed4000 | |||
79bbdd2948 | |||
a03e9bb8ea | |||
9b9cfdb2bf | |||
bb8a6b6c3d | |||
297bf2c554 | |||
05e9b3746e | |||
3b7f81e22c | |||
6edf4e27f1 | |||
11dc5b046e | |||
ddb37d6c3f | |||
aed6478a2c | |||
418b526033 | |||
136a7923c0 | |||
5364f963ae | |||
1ea3e1c15c | |||
8530c85adc | |||
6815dd238b | |||
00562ff82d | |||
b7e8a7a5f5 | |||
6181dd07ad | |||
5fe4d4cfd8 | |||
544ca4b7ed | |||
35d342a478 | |||
29e4c2bbdb | |||
fd0b050686 | |||
d0645ab1a8 | |||
65d7e0e506 |
6
.github/workflows/lint.yml
vendored
6
.github/workflows/lint.yml
vendored
@ -13,13 +13,13 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.11
|
||||||
|
|
||||||
- name: Cache dependencies
|
- name: Cache dependencies
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache
|
~/.cache
|
||||||
|
759
.github/workflows/test-devel.yml
vendored
759
.github/workflows/test-devel.yml
vendored
File diff suppressed because it is too large
Load Diff
117
.github/workflows/test-devel_release.yml
vendored
117
.github/workflows/test-devel_release.yml
vendored
@ -3,12 +3,12 @@
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-anope:
|
build-anope:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- 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@v2
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
key: 3-${{ runner.os }}-anope-devel_release
|
key: 3-${{ runner.os }}-anope-devel_release
|
||||||
path: '~/.cache
|
path: '~/.cache
|
||||||
@ -16,44 +16,44 @@ jobs:
|
|||||||
${ github.workspace }/anope
|
${ github.workspace }/anope
|
||||||
|
|
||||||
'
|
'
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.11
|
||||||
- name: Checkout Anope
|
- name: Checkout Anope
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
path: anope
|
path: anope
|
||||||
ref: 2.0.9
|
ref: '2.0'
|
||||||
repository: anope/anope
|
repository: anope/anope
|
||||||
- name: Build Anope
|
- name: Build Anope
|
||||||
run: |
|
run: |
|
||||||
cd $GITHUB_WORKSPACE/anope/
|
cd $GITHUB_WORKSPACE/anope/
|
||||||
cp $GITHUB_WORKSPACE/data/anope/* .
|
sudo apt-get install ninja-build --no-install-recommends
|
||||||
CFLAGS=-O0 ./Config -quick
|
mkdir build && cd build
|
||||||
make -C build -j 4
|
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local/ -DPROGRAM_NAME=anope -DUSE_PCH=ON -GNinja ..
|
||||||
make -C build install
|
ninja 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@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: installed-anope
|
name: installed-anope
|
||||||
path: ~/artefacts-*.tar.gz
|
path: ~/artefacts-*.tar.gz
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
build-inspircd:
|
build-inspircd:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Create directories
|
- name: Create directories
|
||||||
run: cd ~/; mkdir -p .local/ go/
|
run: cd ~/; mkdir -p .local/ go/
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.11
|
||||||
- name: Checkout InspIRCd
|
- name: Checkout InspIRCd
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
path: inspircd
|
path: inspircd
|
||||||
ref: insp3
|
ref: insp3
|
||||||
@ -61,14 +61,13 @@ jobs:
|
|||||||
- name: Build InspIRCd
|
- name: Build InspIRCd
|
||||||
run: |
|
run: |
|
||||||
cd $GITHUB_WORKSPACE/inspircd/
|
cd $GITHUB_WORKSPACE/inspircd/
|
||||||
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch
|
|
||||||
./configure --prefix=$HOME/.local/inspircd --development
|
./configure --prefix=$HOME/.local/inspircd --development
|
||||||
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@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: ~/artefacts-*.tar.gz
|
path: ~/artefacts-*.tar.gz
|
||||||
@ -80,11 +79,11 @@ jobs:
|
|||||||
- test-inspircd
|
- test-inspircd
|
||||||
- test-inspircd-anope
|
- test-inspircd-anope
|
||||||
- test-inspircd-atheme
|
- test-inspircd-atheme
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Download Artifacts
|
- name: Download Artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: Install dashboard dependencies
|
- name: Install dashboard dependencies
|
||||||
@ -107,15 +106,15 @@ jobs:
|
|||||||
test-inspircd:
|
test-inspircd:
|
||||||
needs:
|
needs:
|
||||||
- build-inspircd
|
- build-inspircd
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.11
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: '~'
|
path: '~'
|
||||||
@ -126,14 +125,16 @@ 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 -r requirements.txt
|
pip install pytest pytest-xdist pytest-timeout -r requirements.txt
|
||||||
- name: Test with pytest
|
- env:
|
||||||
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
|
IRCTEST_DEBUG_LOGS: ${{ runner.debug }}
|
||||||
|
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@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: pytest-results_inspircd_devel_release
|
name: pytest-results_inspircd_devel_release
|
||||||
path: pytest.xml
|
path: pytest.xml
|
||||||
@ -141,20 +142,20 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- build-inspircd
|
- build-inspircd
|
||||||
- build-anope
|
- build-anope
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.11
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: '~'
|
path: '~'
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: installed-anope
|
name: installed-anope
|
||||||
path: '~'
|
path: '~'
|
||||||
@ -165,29 +166,31 @@ 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 -r requirements.txt
|
pip install pytest pytest-xdist pytest-timeout -r requirements.txt
|
||||||
- name: Test with pytest
|
- env:
|
||||||
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make
|
IRCTEST_DEBUG_LOGS: ${{ runner.debug }}
|
||||||
|
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@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: pytest-results_inspircd-anope_devel_release
|
name: pytest-results_inspircd-anope_devel_release
|
||||||
path: pytest.xml
|
path: pytest.xml
|
||||||
test-inspircd-atheme:
|
test-inspircd-atheme:
|
||||||
needs:
|
needs:
|
||||||
- build-inspircd
|
- build-inspircd
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v4
|
||||||
- name: Set up Python 3.7
|
- name: Set up Python 3.11
|
||||||
uses: actions/setup-python@v2
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: 3.7
|
python-version: 3.11
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: '~'
|
path: '~'
|
||||||
@ -198,14 +201,16 @@ 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 -r requirements.txt
|
pip install pytest pytest-xdist pytest-timeout -r requirements.txt
|
||||||
- name: Test with pytest
|
- env:
|
||||||
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
|
IRCTEST_DEBUG_LOGS: ${{ runner.debug }}
|
||||||
|
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@v2
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: pytest-results_inspircd-atheme_devel_release
|
name: pytest-results_inspircd-atheme_devel_release
|
||||||
path: pytest.xml
|
path: pytest.xml
|
||||||
|
801
.github/workflows/test-stable.yml
vendored
801
.github/workflows/test-stable.yml
vendored
File diff suppressed because it is too large
Load Diff
@ -2,22 +2,23 @@ exclude: ^irctest/scram
|
|||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 22.3.0
|
rev: 23.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3
|
language_version: python3
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/isort
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: 5.5.2
|
rev: 5.11.5
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
|
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 5.0.4
|
rev: 5.0.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v0.812
|
rev: v1.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
|
additional_dependencies: [types-PyYAML, types-docutils]
|
||||||
|
43
Makefile
43
Makefile
@ -35,22 +35,18 @@ INSPIRCD_SELECTORS := \
|
|||||||
and not strict \
|
and not strict \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
# HelpTestCase fails because it returns NOTICEs instead of numerics
|
|
||||||
IRCU2_SELECTORS := \
|
IRCU2_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
# same justification as ircu2
|
|
||||||
# lusers "unregistered" tests fail because
|
|
||||||
NEFARIOUS_SELECTORS := \
|
NEFARIOUS_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
# same justification as ircu2
|
|
||||||
SNIRCD_SELECTORS := \
|
SNIRCD_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
@ -87,6 +83,16 @@ 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
|
||||||
|
SABLE_SELECTORS := \
|
||||||
|
not Ergo \
|
||||||
|
and not deprecated \
|
||||||
|
and not strict \
|
||||||
|
and not arbitrary_client_tags \
|
||||||
|
and not react_tag \
|
||||||
|
and not list and not lusers and not time and not info \
|
||||||
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
SOLANUM_SELECTORS := \
|
SOLANUM_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
@ -98,6 +104,13 @@ SOPEL_SELECTORS := \
|
|||||||
(foo or not foo) \
|
(foo or not foo) \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
|
# TheLounge can actually pass all the test so there is none to exclude.
|
||||||
|
# `(foo or not foo)` serves as a `true` value so it doesn't break when
|
||||||
|
# $(EXTRA_SELECTORS) is non-empty
|
||||||
|
THELOUNGE_SELECTORS := \
|
||||||
|
(foo or not foo) \
|
||||||
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
# Tests marked with arbitrary_client_tags can't pass because Unreal whitelists which tags it relays
|
# Tests marked with arbitrary_client_tags can't pass because Unreal whitelists which tags it relays
|
||||||
# Tests marked with react_tag can't pass because Unreal blocks +draft/react https://github.com/unrealircd/unrealircd/pull/149
|
# Tests marked with react_tag can't pass because Unreal blocks +draft/react https://github.com/unrealircd/unrealircd/pull/149
|
||||||
# Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs
|
# Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs
|
||||||
@ -111,9 +124,9 @@ UNREALIRCD_SELECTORS := \
|
|||||||
and not private_chathistory \
|
and not private_chathistory \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd
|
.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sable sopel solanum unrealircd
|
||||||
|
|
||||||
all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd
|
all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sable sopel solanum unrealircd
|
||||||
|
|
||||||
flakes:
|
flakes:
|
||||||
find irctest/ -name "*.py" -not -path "irctest/scram/*" -print0 | xargs -0 pyflakes3
|
find irctest/ -name "*.py" -not -path "irctest/scram/*" -print0 | xargs -0 pyflakes3
|
||||||
@ -242,6 +255,12 @@ ngircd-atheme:
|
|||||||
-m 'services' \
|
-m 'services' \
|
||||||
-k "$(NGIRCD_SELECTORS)"
|
-k "$(NGIRCD_SELECTORS)"
|
||||||
|
|
||||||
|
sable:
|
||||||
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
|
--controller=irctest.controllers.sable \
|
||||||
|
-n 20 \
|
||||||
|
-k '$(SABLE_SELECTORS)'
|
||||||
|
|
||||||
solanum:
|
solanum:
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller=irctest.controllers.solanum \
|
--controller=irctest.controllers.solanum \
|
||||||
@ -253,6 +272,11 @@ sopel:
|
|||||||
--controller=irctest.controllers.sopel \
|
--controller=irctest.controllers.sopel \
|
||||||
-k '$(SOPEL_SELECTORS)'
|
-k '$(SOPEL_SELECTORS)'
|
||||||
|
|
||||||
|
thelounge:
|
||||||
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
|
--controller=irctest.controllers.thelounge \
|
||||||
|
-k '$(THELOUNGE_SELECTORS)'
|
||||||
|
|
||||||
unrealircd:
|
unrealircd:
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller=irctest.controllers.unrealircd \
|
--controller=irctest.controllers.unrealircd \
|
||||||
@ -274,3 +298,10 @@ unrealircd-anope:
|
|||||||
--services-controller=irctest.controllers.anope_services \
|
--services-controller=irctest.controllers.anope_services \
|
||||||
-m 'services' \
|
-m 'services' \
|
||||||
-k '$(UNREALIRCD_SELECTORS)'
|
-k '$(UNREALIRCD_SELECTORS)'
|
||||||
|
|
||||||
|
unrealircd-dlk:
|
||||||
|
pifpaf run mysql -- $(PYTEST) $(PYTEST_ARGS) \
|
||||||
|
--controller=irctest.controllers.unrealircd \
|
||||||
|
--services-controller=irctest.controllers.dlk_services \
|
||||||
|
-m 'services' \
|
||||||
|
-k '$(UNREALIRCD_SELECTORS)'
|
||||||
|
66
README.md
66
README.md
@ -3,16 +3,30 @@
|
|||||||
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
|
* IRC protocol test cases, primarily checking conformance to
|
||||||
* small wrappers around existing software to run tests on them
|
[the "Modern" specification](https://modern.ircdocs.horse/) and
|
||||||
|
[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:
|
||||||
@ -20,7 +34,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
|
||||||
```
|
```
|
||||||
@ -40,18 +54,23 @@ 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: `-k 'not Ergo and not deprecated and not strict`.
|
use these options: `-m '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
|
||||||
@ -63,6 +82,10 @@ 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:
|
||||||
@ -89,20 +112,6 @@ 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:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -110,8 +119,11 @@ cd /tmp/
|
|||||||
git clone https://github.com/inspircd/inspircd.git
|
git clone https://github.com/inspircd/inspircd.git
|
||||||
cd inspircd
|
cd inspircd
|
||||||
|
|
||||||
# optional, makes tests run considerably faster
|
# Optional, makes tests run considerably faster. Pick one depending on the InspIRCd version:
|
||||||
|
# on Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21:
|
||||||
patch src/inspircd.cpp < ~/irctest/patches/inspircd_mainloop.patch
|
patch src/inspircd.cpp < ~/irctest/patches/inspircd_mainloop.patch
|
||||||
|
# on Insp3 >= 3.17.0 and Insp4 >= 4.0.0a22:
|
||||||
|
export CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP
|
||||||
|
|
||||||
./configure --prefix=$HOME/.local/ --development
|
./configure --prefix=$HOME/.local/ --development
|
||||||
make -j 4
|
make -j 4
|
||||||
@ -120,14 +132,6 @@ 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:
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -144,8 +148,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), most server controllers can optionally run
|
Besides Ergo (that has built-in services) and Sable (that ships its own services),
|
||||||
service packages.
|
most server controllers can optionally run service packages.
|
||||||
|
|
||||||
#### Atheme:
|
#### Atheme:
|
||||||
|
|
||||||
|
11
conftest.py
11
conftest.py
@ -106,13 +106,10 @@ def pytest_collection_modifyitems(session, config, items):
|
|||||||
assert isinstance(item, _pytest.python.Function)
|
assert isinstance(item, _pytest.python.Function)
|
||||||
|
|
||||||
# unittest-style test functions have the node of UnitTest class as parent
|
# unittest-style test functions have the node of UnitTest class as parent
|
||||||
assert isinstance(
|
if tuple(map(int, _pytest.__version__.split("."))) >= (7,):
|
||||||
item.parent,
|
assert isinstance(item.parent, _pytest.python.Class)
|
||||||
(
|
else:
|
||||||
_pytest.python.Class, # pytest >= 7.0.0
|
assert isinstance(item.parent, _pytest.python.Instance)
|
||||||
_pytest.python.Instance, # pytest < 7.0.0
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
# and that node references the UnitTest class
|
# and that node references the UnitTest class
|
||||||
assert issubclass(item.parent.cls, _IrcTestCase)
|
assert issubclass(item.parent.cls, _IrcTestCase)
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
INSTDIR="$HOME/.local/"
|
|
||||||
RUNGROUP=""
|
|
||||||
UMASK=077
|
|
||||||
DEBUG="yes"
|
|
||||||
USE_PCH="yes"
|
|
||||||
EXTRA_INCLUDE_DIRS=""
|
|
||||||
EXTRA_LIB_DIRS=""
|
|
||||||
EXTRA_CONFIG_ARGS=""
|
|
@ -19,6 +19,10 @@ SHOWLISTMODES="1"
|
|||||||
NOOPEROVERRIDE=""
|
NOOPEROVERRIDE=""
|
||||||
OPEROVERRIDEVERIFY=""
|
OPEROVERRIDEVERIFY=""
|
||||||
GENCERTIFICATE="1"
|
GENCERTIFICATE="1"
|
||||||
EXTRAPARA=""
|
|
||||||
|
# Use system argon to avoid getting SIGILLed if the build machine has a more recent
|
||||||
|
# CPU than the one running the tests.
|
||||||
|
EXTRAPARA="--with-system-argon2"
|
||||||
|
|
||||||
ADVANCED=""
|
ADVANCED=""
|
||||||
|
|
||||||
|
@ -1,19 +1,36 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import contextlib
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import textwrap
|
||||||
import time
|
import time
|
||||||
from typing import IO, Any, Callable, Dict, List, Optional, Set, Tuple, Type
|
from typing import (
|
||||||
|
IO,
|
||||||
|
Any,
|
||||||
|
Callable,
|
||||||
|
Dict,
|
||||||
|
Iterator,
|
||||||
|
List,
|
||||||
|
Optional,
|
||||||
|
Sequence,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
import irctest
|
import irctest
|
||||||
|
|
||||||
from . import authentication, tls
|
from . import authentication, tls
|
||||||
from .client_mock import ClientMock
|
from .client_mock import ClientMock
|
||||||
|
from .irc_utils.filelock import FileLock
|
||||||
from .irc_utils.junkdrawer import find_hostname_and_port
|
from .irc_utils.junkdrawer import find_hostname_and_port
|
||||||
from .irc_utils.message_parser import Message
|
from .irc_utils.message_parser import Message
|
||||||
from .runner import NotImplementedByController
|
from .runner import NotImplementedByController
|
||||||
@ -34,6 +51,14 @@ 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."""
|
||||||
|
|
||||||
@ -55,17 +80,48 @@ class _BaseController:
|
|||||||
|
|
||||||
supports_sts: bool
|
supports_sts: bool
|
||||||
supported_sasl_mechanisms: Set[str]
|
supported_sasl_mechanisms: Set[str]
|
||||||
|
|
||||||
proc: Optional[subprocess.Popen]
|
proc: Optional[subprocess.Popen]
|
||||||
|
|
||||||
|
_used_ports_path = Path(tempfile.gettempdir()) / "irctest_ports.json"
|
||||||
|
_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()
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def _used_ports(self) -> Iterator[Set[Tuple[str, int]]]:
|
||||||
|
with self._port_lock:
|
||||||
|
if not self._used_ports_path.exists():
|
||||||
|
self._used_ports_path.write_text("[]")
|
||||||
|
used_ports = {
|
||||||
|
(h, p) for (h, p) in json.loads(self._used_ports_path.read_text())
|
||||||
|
}
|
||||||
|
yield used_ports
|
||||||
|
self._used_ports_path.write_text(json.dumps(list(used_ports)))
|
||||||
|
|
||||||
|
def get_hostname_and_port(self) -> Tuple[str, int]:
|
||||||
|
with self._used_ports() as used_ports:
|
||||||
|
while True:
|
||||||
|
(hostname, port) = find_hostname_and_port()
|
||||||
|
if (hostname, port) not in used_ports:
|
||||||
|
# double-checking in self._used_ports to prevent collisions
|
||||||
|
# between controllers starting at the same time.
|
||||||
|
break
|
||||||
|
|
||||||
|
used_ports.add((hostname, port))
|
||||||
|
self._own_ports.add((hostname, port))
|
||||||
|
|
||||||
|
return (hostname, port)
|
||||||
|
|
||||||
def check_is_alive(self) -> None:
|
def check_is_alive(self) -> None:
|
||||||
assert self.proc
|
assert self.proc
|
||||||
self.proc.poll()
|
self.proc.poll()
|
||||||
if self.proc.returncode is not None:
|
if self.proc.returncode is not None:
|
||||||
raise ProcessStopped()
|
raise ProcessStopped(f"process returned {self.proc.returncode}")
|
||||||
|
|
||||||
def kill_proc(self) -> None:
|
def kill_proc(self) -> None:
|
||||||
"""Terminates the controlled process, waits for it to exit, and
|
"""Terminates the controlled process, waits for it to exit, and
|
||||||
@ -83,6 +139,17 @@ class _BaseController:
|
|||||||
if self.proc:
|
if self.proc:
|
||||||
self.kill_proc()
|
self.kill_proc()
|
||||||
|
|
||||||
|
with self._used_ports() as used_ports:
|
||||||
|
for hostname, port in list(self._own_ports):
|
||||||
|
used_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
|
||||||
@ -156,10 +223,18 @@ class DirectoryBasedController(_BaseController):
|
|||||||
],
|
],
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
subprocess.check_output(
|
with self.dh_path.open("w") as fd:
|
||||||
[self.openssl_bin, "dhparam", "-out", self.dh_path, "128"],
|
fd.write(
|
||||||
stderr=subprocess.DEVNULL,
|
textwrap.dedent(
|
||||||
)
|
"""
|
||||||
|
-----BEGIN DH PARAMETERS-----
|
||||||
|
MIGHAoGBAJICSyQAiLj1fw8b5xELcnpqBQ+wvOyKgim4IetWOgZnRQFkTgOeoRZD
|
||||||
|
HksACRFJL/EqHxDKcy/2Ghwr2axhNxSJ+UOBmraP3WfodV/fCDPnZ+XnI9fjHsIr
|
||||||
|
rjisPMqomjXeiTB1UeAHvLUmCK4yx6lpAJsCYwJjsqkycUfHiy1bAgEC
|
||||||
|
-----END DH PARAMETERS-----
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseClientController(_BaseController):
|
class BaseClientController(_BaseController):
|
||||||
@ -188,14 +263,17 @@ class BaseServerController(_BaseController):
|
|||||||
extban_mute_char: Optional[str] = None
|
extban_mute_char: Optional[str] = None
|
||||||
"""Character used for the 'mute' extban"""
|
"""Character used for the 'mute' extban"""
|
||||||
nickserv = "NickServ"
|
nickserv = "NickServ"
|
||||||
|
sync_sleep_time = 0.0
|
||||||
|
"""How many seconds to sleep before clients synchronously get messages.
|
||||||
|
|
||||||
|
This can be 0 for servers answering all commands in order (all but Sable as of
|
||||||
|
this writing), as irctest emits a PING, waits for a PONG, and captures all messages
|
||||||
|
between the two."""
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any):
|
def __init__(self, *args: Any, **kwargs: Any):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.faketime_enabled = False
|
self.faketime_enabled = False
|
||||||
|
|
||||||
def get_hostname_and_port(self) -> Tuple[str, int]:
|
|
||||||
return find_hostname_and_port()
|
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
hostname: str,
|
hostname: str,
|
||||||
@ -204,8 +282,6 @@ class BaseServerController(_BaseController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]],
|
|
||||||
invalid_metadata_keys: Optional[Set[str]],
|
|
||||||
faketime: Optional[str],
|
faketime: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
@ -241,7 +317,7 @@ class BaseServerController(_BaseController):
|
|||||||
time.sleep(0.01)
|
time.sleep(0.01)
|
||||||
|
|
||||||
c.send(b" ") # Triggers BrokenPipeError
|
c.send(b" ") # Triggers BrokenPipeError
|
||||||
except BrokenPipeError:
|
except (BrokenPipeError, ConnectionResetError):
|
||||||
# ircu2 cuts the connection without a message if registration
|
# ircu2 cuts the connection without a message if registration
|
||||||
# is not complete.
|
# is not complete.
|
||||||
pass
|
pass
|
||||||
@ -295,25 +371,38 @@ class BaseServicesController(_BaseController):
|
|||||||
c.connect(self.server_controller.hostname, self.server_controller.port)
|
c.connect(self.server_controller.hostname, self.server_controller.port)
|
||||||
c.sendLine("NICK chkNS")
|
c.sendLine("NICK chkNS")
|
||||||
c.sendLine("USER chk chk chk chk")
|
c.sendLine("USER chk chk chk chk")
|
||||||
for msg in c.getMessages(synchronize=False):
|
time.sleep(self.server_controller.sync_sleep_time)
|
||||||
if msg.command == "PING":
|
got_end_of_motd = False
|
||||||
# Hi Unreal
|
while not got_end_of_motd:
|
||||||
c.sendLine("PONG :" + msg.params[0])
|
for msg in c.getMessages(synchronize=False):
|
||||||
c.getMessages()
|
if msg.command == "PING":
|
||||||
|
# Hi Unreal
|
||||||
|
c.sendLine("PONG :" + msg.params[0])
|
||||||
|
if msg.command in ("376", "422"): # RPL_ENDOFMOTD / ERR_NOMOTD
|
||||||
|
got_end_of_motd = True
|
||||||
|
|
||||||
timeout = time.time() + 5
|
timeout = time.time() + 10
|
||||||
while True:
|
while True:
|
||||||
c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :HELP")
|
c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :help")
|
||||||
msgs = self.getNickServResponse(c)
|
|
||||||
|
msgs = self.getNickServResponse(c, timeout=1)
|
||||||
for msg in msgs:
|
for msg in msgs:
|
||||||
if msg.command == "401":
|
if msg.command == "401":
|
||||||
# NickServ not available yet
|
# NickServ not available yet
|
||||||
pass
|
pass
|
||||||
|
elif msg.command in ("MODE", "221"): # RPL_UMODEIS
|
||||||
|
pass
|
||||||
|
elif msg.command == "396": # RPL_VISIBLEHOST
|
||||||
|
pass
|
||||||
elif msg.command == "NOTICE":
|
elif msg.command == "NOTICE":
|
||||||
# NickServ is available
|
assert msg.prefix is not None
|
||||||
assert "nickserv" in (msg.prefix or "").lower(), msg
|
if "!" not in msg.prefix and "." in msg.prefix:
|
||||||
print("breaking")
|
# Server notice
|
||||||
break
|
pass
|
||||||
|
else:
|
||||||
|
# NickServ is available
|
||||||
|
assert "nickserv" in (msg.prefix or "").lower(), msg
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
assert False, f"unexpected reply from NickServ: {msg}"
|
assert False, f"unexpected reply from NickServ: {msg}"
|
||||||
else:
|
else:
|
||||||
@ -330,11 +419,12 @@ class BaseServicesController(_BaseController):
|
|||||||
c.disconnect()
|
c.disconnect()
|
||||||
self.services_up = True
|
self.services_up = True
|
||||||
|
|
||||||
def getNickServResponse(self, client: Any) -> List[Message]:
|
def getNickServResponse(self, client: Any, timeout: int = 0) -> List[Message]:
|
||||||
"""Wrapper aroung getMessages() that waits longer, because NickServ
|
"""Wrapper aroung getMessages() that waits longer, because NickServ
|
||||||
is queried asynchronously."""
|
is queried asynchronously."""
|
||||||
msgs: List[Message] = []
|
msgs: List[Message] = []
|
||||||
while not msgs:
|
start_time = time.time()
|
||||||
|
while not msgs and (not timeout or start_time + timeout > time.time()):
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
msgs = client.getMessages()
|
msgs = client.getMessages()
|
||||||
return msgs
|
return msgs
|
||||||
|
@ -160,6 +160,7 @@ 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[
|
||||||
@ -173,7 +174,7 @@ class _IrcTestCase(Generic[TController]):
|
|||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""Returns an error message if the message doesn't match the given arguments,
|
"""Returns an error message if the message doesn't match the given arguments,
|
||||||
or None if it matches."""
|
or None if it matches."""
|
||||||
for (key, value) in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
if getattr(msg, key) != value:
|
if getattr(msg, key) != value:
|
||||||
fail_msg = (
|
fail_msg = (
|
||||||
fail_msg or "expected {param} to be {expects}, got {got}: {msg}"
|
fail_msg or "expected {param} to be {expects}, got {got}: {msg}"
|
||||||
@ -186,6 +187,14 @@ 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}"
|
||||||
@ -214,7 +223,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, param=key, msg=msg
|
*extra_format, got=got_nick, expects=nick, msg=msg
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
@ -351,8 +360,8 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
|
|||||||
nick: Optional[str] = None
|
nick: Optional[str] = None
|
||||||
user: Optional[List[str]] = None
|
user: Optional[List[str]] = None
|
||||||
server: socket.socket
|
server: socket.socket
|
||||||
protocol_version = Optional[str]
|
protocol_version: Optional[str]
|
||||||
acked_capabilities = Optional[Set[str]]
|
acked_capabilities: Optional[Set[str]]
|
||||||
|
|
||||||
__new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise
|
__new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise
|
||||||
|
|
||||||
@ -448,7 +457,9 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
|
|||||||
print("{:.3f} S: {}".format(time.time(), line.strip()))
|
print("{:.3f} S: {}".format(time.time(), line.strip()))
|
||||||
|
|
||||||
def readCapLs(
|
def readCapLs(
|
||||||
self, auth: Optional[Authentication] = None, tls_config: tls.TlsConfig = None
|
self,
|
||||||
|
auth: Optional[Authentication] = None,
|
||||||
|
tls_config: Optional[tls.TlsConfig] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
(hostname, port) = self.server.getsockname()
|
(hostname, port) = self.server.getsockname()
|
||||||
self.controller.run(
|
self.controller.run(
|
||||||
@ -458,9 +469,9 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
|
|||||||
m = self.getMessage()
|
m = self.getMessage()
|
||||||
self.assertEqual(m.command, "CAP", "First message is not CAP LS.")
|
self.assertEqual(m.command, "CAP", "First message is not CAP LS.")
|
||||||
if m.params == ["LS"]:
|
if m.params == ["LS"]:
|
||||||
self.protocol_version = 301
|
self.protocol_version = "301"
|
||||||
elif m.params == ["LS", "302"]:
|
elif m.params == ["LS", "302"]:
|
||||||
self.protocol_version = 302
|
self.protocol_version = "302"
|
||||||
elif m.params == ["END"]:
|
elif m.params == ["END"]:
|
||||||
self.protocol_version = None
|
self.protocol_version = None
|
||||||
else:
|
else:
|
||||||
@ -527,8 +538,6 @@ class BaseServerTestCase(
|
|||||||
|
|
||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
ssl = False
|
ssl = False
|
||||||
valid_metadata_keys: Set[str] = set()
|
|
||||||
invalid_metadata_keys: Set[str] = set()
|
|
||||||
server_support: Optional[Dict[str, Optional[str]]]
|
server_support: Optional[Dict[str, Optional[str]]]
|
||||||
run_services = False
|
run_services = False
|
||||||
|
|
||||||
@ -548,8 +557,6 @@ class BaseServerTestCase(
|
|||||||
self.hostname,
|
self.hostname,
|
||||||
self.port,
|
self.port,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
valid_metadata_keys=self.valid_metadata_keys,
|
|
||||||
invalid_metadata_keys=self.invalid_metadata_keys,
|
|
||||||
ssl=self.ssl,
|
ssl=self.ssl,
|
||||||
run_services=self.run_services,
|
run_services=self.run_services,
|
||||||
faketime=self.faketime,
|
faketime=self.faketime,
|
||||||
@ -587,9 +594,13 @@ class BaseServerTestCase(
|
|||||||
del self.clients[name]
|
del self.clients[name]
|
||||||
|
|
||||||
def getMessages(self, client: TClientName, **kwargs: Any) -> List[Message]:
|
def getMessages(self, client: TClientName, **kwargs: Any) -> List[Message]:
|
||||||
|
if kwargs.get("synchronize", True):
|
||||||
|
time.sleep(self.controller.sync_sleep_time)
|
||||||
return self.clients[client].getMessages(**kwargs)
|
return self.clients[client].getMessages(**kwargs)
|
||||||
|
|
||||||
def getMessage(self, client: TClientName, **kwargs: Any) -> Message:
|
def getMessage(self, client: TClientName, **kwargs: Any) -> Message:
|
||||||
|
if kwargs.get("synchronize", True):
|
||||||
|
time.sleep(self.controller.sync_sleep_time)
|
||||||
return self.clients[client].getMessage(**kwargs)
|
return self.clients[client].getMessage(**kwargs)
|
||||||
|
|
||||||
def getRegistrationMessage(self, client: TClientName) -> Message:
|
def getRegistrationMessage(self, client: TClientName) -> Message:
|
||||||
@ -689,7 +700,7 @@ class BaseServerTestCase(
|
|||||||
def connectClient(
|
def connectClient(
|
||||||
self,
|
self,
|
||||||
nick: str,
|
nick: str,
|
||||||
name: TClientName = None,
|
name: Optional[TClientName] = None,
|
||||||
capabilities: Optional[List[str]] = None,
|
capabilities: Optional[List[str]] = None,
|
||||||
skip_if_cap_nak: bool = False,
|
skip_if_cap_nak: bool = False,
|
||||||
show_io: Optional[bool] = None,
|
show_io: Optional[bool] = None,
|
||||||
@ -734,8 +745,8 @@ class BaseServerTestCase(
|
|||||||
self.server_support[param] = None
|
self.server_support[param] = None
|
||||||
welcome.append(m)
|
welcome.append(m)
|
||||||
|
|
||||||
self.targmax: Dict[str, Optional[str]] = dict(
|
self.targmax: Dict[str, Optional[str]] = dict( # type: ignore[assignment]
|
||||||
item.split(":", 1) # type: ignore
|
item.split(":", 1)
|
||||||
for item in (self.server_support.get("TARGMAX") or "").split(",")
|
for item in (self.server_support.get("TARGMAX") or "").split(",")
|
||||||
if item
|
if item
|
||||||
)
|
)
|
||||||
@ -800,7 +811,7 @@ def xfailIf(
|
|||||||
def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]:
|
def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]:
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn:
|
def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn:
|
||||||
if condition(self):
|
if condition(self, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
return f(self, *args, **kwargs)
|
return f(self, *args, **kwargs)
|
||||||
except Exception:
|
except Exception:
|
||||||
@ -817,7 +828,10 @@ def xfailIf(
|
|||||||
def xfailIfSoftware(
|
def xfailIfSoftware(
|
||||||
names: List[str], reason: str
|
names: List[str], reason: str
|
||||||
) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]:
|
) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]:
|
||||||
return xfailIf(lambda testcase: testcase.controller.software_name in names, reason)
|
def pred(testcase: _IrcTestCase, *args: Any, **kwargs: Any) -> bool:
|
||||||
|
return testcase.controller.software_name in names
|
||||||
|
|
||||||
|
return xfailIf(pred, reason)
|
||||||
|
|
||||||
|
|
||||||
def mark_services(cls: TClass) -> TClass:
|
def mark_services(cls: TClass) -> TClass:
|
||||||
|
@ -228,7 +228,7 @@ class SaslTestCase(cases.BaseClientTestCase):
|
|||||||
self.assertEqual(m.params, ["+"], m)
|
self.assertEqual(m.params, ["+"], m)
|
||||||
|
|
||||||
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
|
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
|
||||||
def testScramBadPassword(self):
|
def testScramBadPassword(self, server_fakes_success=False, fake_response=None):
|
||||||
"""Test SCRAM-SHA-256 authentication with a bad password."""
|
"""Test SCRAM-SHA-256 authentication with a bad password."""
|
||||||
auth = authentication.Authentication(
|
auth = authentication.Authentication(
|
||||||
mechanisms=[authentication.Mechanisms.scram_sha_256],
|
mechanisms=[authentication.Mechanisms.scram_sha_256],
|
||||||
@ -261,6 +261,36 @@ class SaslTestCase(cases.BaseClientTestCase):
|
|||||||
with self.assertRaises(scram.NotAuthorizedException):
|
with self.assertRaises(scram.NotAuthorizedException):
|
||||||
authenticator.response(msg)
|
authenticator.response(msg)
|
||||||
|
|
||||||
|
if server_fakes_success:
|
||||||
|
self.sendLine(f"AUTHENTICATE :{fake_response}")
|
||||||
|
|
||||||
|
m = self.getMessage()
|
||||||
|
while m.command == "PING":
|
||||||
|
self.sendLine(f"PONG server. {m.params[-1]}")
|
||||||
|
m = self.getMessage()
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="AUTHENTICATE",
|
||||||
|
params=["*"],
|
||||||
|
fail_msg="Client did not abort: {msg}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"fake_response",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"AAAA",
|
||||||
|
"dj1ubU1mM1FIV2NKUWk5cE1ndHFLU0tQclZueUk2c3FOTzZJN3BFLzBveUdjPQ==",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def testScramMaliciousServer(self, fake_response):
|
||||||
|
"""Test SCRAM-SHA-256 authentication to a server which pretends to know
|
||||||
|
the password"""
|
||||||
|
self.testScramBadPassword(
|
||||||
|
server_fakes_success=True, fake_response=fake_response
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Irc302SaslTestCase(cases.BaseClientTestCase):
|
class Irc302SaslTestCase(cases.BaseClientTestCase):
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.skipUnlessHasMechanism("PLAIN")
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
|
import functools
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Type
|
from typing import Tuple, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import BaseServicesController, DirectoryBasedController
|
from irctest.basecontrollers import BaseServicesController, DirectoryBasedController
|
||||||
|
|
||||||
TEMPLATE_CONFIG = """
|
TEMPLATE_CONFIG = """
|
||||||
serverinfo {{
|
serverinfo {{
|
||||||
name = "services.example.org"
|
name = "My.Little.Services"
|
||||||
description = "Anope IRC Services"
|
description = "Anope IRC Services"
|
||||||
numeric = "00A"
|
numeric = "00A"
|
||||||
pid = "services.pid"
|
pid = "services.pid"
|
||||||
@ -48,6 +49,8 @@ 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"; }}
|
||||||
|
|
||||||
@ -63,17 +66,33 @@ options {{
|
|||||||
warningtimeout = 4h
|
warningtimeout = 4h
|
||||||
}}
|
}}
|
||||||
|
|
||||||
module {{ name = "m_sasl" }}
|
module {{ name = "ns_sasl" }} # since 2.1.13
|
||||||
module {{ name = "enc_sha256" }}
|
module {{ name = "sasl" }} # 2.1.2 to 2.1.12
|
||||||
|
module {{ name = "m_sasl" }} # 2.0 to 2.1.1
|
||||||
|
|
||||||
|
module {{ name = "enc_sha2" }} # 2.1
|
||||||
|
module {{ name = "enc_sha256" }} # 2.0
|
||||||
|
|
||||||
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()
|
||||||
@ -88,6 +107,21 @@ 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(
|
||||||
@ -100,24 +134,25 @@ class AnopeController(BaseServicesController, DirectoryBasedController):
|
|||||||
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"
|
||||||
|
)
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
extra_args = []
|
||||||
|
if self.debug_mode:
|
||||||
|
extra_args.append("--debug")
|
||||||
|
|
||||||
|
self.proc = self.execute(
|
||||||
[
|
[
|
||||||
"services",
|
"anope",
|
||||||
"-n", # don't fork
|
"--config=services.conf", # can't be an absolute path in 2.0
|
||||||
"--config=services.conf", # can't be an absolute path
|
"--nofork", # don't fork
|
||||||
# "--logdir",
|
"--nopid", # don't write a pid
|
||||||
# f"/tmp/services-{server_port}.log",
|
*extra_args,
|
||||||
],
|
],
|
||||||
cwd=self.directory,
|
cwd=self.directory,
|
||||||
# stdout=subprocess.DEVNULL,
|
|
||||||
# stderr=subprocess.DEVNULL,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import subprocess
|
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
import irctest
|
import irctest
|
||||||
@ -25,7 +24,7 @@ loadmodule "modules/saslserv/plain";
|
|||||||
#loadmodule "modules/saslserv/scram";
|
#loadmodule "modules/saslserv/scram";
|
||||||
|
|
||||||
serverinfo {{
|
serverinfo {{
|
||||||
name = "services.example.org";
|
name = "My.Little.Services";
|
||||||
desc = "Atheme IRC Services";
|
desc = "Atheme IRC Services";
|
||||||
numeric = "00A";
|
numeric = "00A";
|
||||||
netname = "testnet";
|
netname = "testnet";
|
||||||
@ -75,7 +74,7 @@ class AthemeController(BaseServicesController, DirectoryBasedController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert self.directory
|
assert self.directory
|
||||||
self.proc = subprocess.Popen(
|
self.proc = self.execute(
|
||||||
[
|
[
|
||||||
"atheme-services",
|
"atheme-services",
|
||||||
"-n", # don't fork
|
"-n", # don't fork
|
||||||
@ -88,8 +87,6 @@ class AthemeController(BaseServicesController, DirectoryBasedController):
|
|||||||
"-D",
|
"-D",
|
||||||
self.directory,
|
self.directory,
|
||||||
],
|
],
|
||||||
# stdout=subprocess.DEVNULL,
|
|
||||||
# stderr=subprocess.DEVNULL,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def registerUser(
|
def registerUser(
|
||||||
|
@ -1,14 +1,8 @@
|
|||||||
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 (
|
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
||||||
BaseServerController,
|
|
||||||
DirectoryBasedController,
|
|
||||||
NotImplementedByController,
|
|
||||||
)
|
|
||||||
from irctest.irc_utils.junkdrawer import find_hostname_and_port
|
|
||||||
|
|
||||||
TEMPLATE_CONFIG = """
|
TEMPLATE_CONFIG = """
|
||||||
global {{
|
global {{
|
||||||
@ -20,7 +14,7 @@ options {{
|
|||||||
network_name unconfigured;
|
network_name unconfigured;
|
||||||
allow_split_ops; # Give ops in empty channels
|
allow_split_ops; # Give ops in empty channels
|
||||||
|
|
||||||
services_name services.example.org;
|
services_name My.Little.Services;
|
||||||
|
|
||||||
// if you need to link more than 1 server, uncomment the following line
|
// if you need to link more than 1 server, uncomment the following line
|
||||||
servtype hub;
|
servtype hub;
|
||||||
@ -50,7 +44,7 @@ class {{
|
|||||||
|
|
||||||
/* for services */
|
/* for services */
|
||||||
super {{
|
super {{
|
||||||
"services.example.org";
|
"My.Little.Services";
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
|
||||||
@ -63,7 +57,7 @@ class {{
|
|||||||
|
|
||||||
/* our services */
|
/* our services */
|
||||||
connect {{
|
connect {{
|
||||||
name services.example.org;
|
name My.Little.Services;
|
||||||
host *@127.0.0.1; # unfortunately, masks aren't allowed here
|
host *@127.0.0.1; # unfortunately, masks aren't allowed here
|
||||||
apasswd password;
|
apasswd password;
|
||||||
cpasswd password;
|
cpasswd password;
|
||||||
@ -97,7 +91,7 @@ class BahamutController(BaseServerController, DirectoryBasedController):
|
|||||||
software_name = "Bahamut"
|
software_name = "Bahamut"
|
||||||
supported_sasl_mechanisms: Set[str] = set()
|
supported_sasl_mechanisms: Set[str] = set()
|
||||||
supports_sts = False
|
supports_sts = False
|
||||||
nickserv = "NickServ@services.example.org"
|
nickserv = "NickServ@My.Little.Services"
|
||||||
|
|
||||||
def create_config(self) -> None:
|
def create_config(self) -> None:
|
||||||
super().create_config()
|
super().create_config()
|
||||||
@ -112,21 +106,14 @@ class BahamutController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
invalid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
restricted_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
faketime: Optional[str],
|
faketime: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
if valid_metadata_keys or invalid_metadata_keys:
|
|
||||||
raise NotImplementedByController(
|
|
||||||
"Defining valid and invalid METADATA keys."
|
|
||||||
)
|
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.create_config()
|
self.create_config()
|
||||||
(unused_hostname, unused_port) = find_hostname_and_port()
|
(unused_hostname, unused_port) = self.get_hostname_and_port()
|
||||||
(services_hostname, services_port) = find_hostname_and_port()
|
(services_hostname, services_port) = self.get_hostname_and_port()
|
||||||
|
|
||||||
password_field = "passwd {};".format(password) if password else ""
|
password_field = "passwd {};".format(password) if password else ""
|
||||||
|
|
||||||
@ -162,7 +149,7 @@ class BahamutController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = self.execute(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"ircd",
|
"ircd",
|
||||||
|
@ -1,13 +1,8 @@
|
|||||||
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
from typing import Optional
|
||||||
from typing import Optional, Set
|
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
||||||
BaseServerController,
|
|
||||||
DirectoryBasedController,
|
|
||||||
NotImplementedByController,
|
|
||||||
)
|
|
||||||
from irctest.irc_utils.junkdrawer import find_hostname_and_port
|
|
||||||
|
|
||||||
TEMPLATE_SSL_CONFIG = """
|
TEMPLATE_SSL_CONFIG = """
|
||||||
ssl_private_key = "{key_path}";
|
ssl_private_key = "{key_path}";
|
||||||
@ -41,19 +36,13 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
invalid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
faketime: Optional[str],
|
faketime: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
if valid_metadata_keys or invalid_metadata_keys:
|
|
||||||
raise NotImplementedByController(
|
|
||||||
"Defining valid and invalid METADATA keys."
|
|
||||||
)
|
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.create_config()
|
self.create_config()
|
||||||
(services_hostname, services_port) = find_hostname_and_port()
|
(services_hostname, services_port) = self.get_hostname_and_port()
|
||||||
password_field = 'password = "{}";'.format(password) if password else ""
|
password_field = 'password = "{}";'.format(password) if password else ""
|
||||||
if ssl:
|
if ssl:
|
||||||
self.gen_ssl()
|
self.gen_ssl()
|
||||||
@ -62,6 +51,8 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
ssl_config = ""
|
ssl_config = ""
|
||||||
|
binary_path = shutil.which(self.binary_name)
|
||||||
|
assert binary_path, f"Could not find '{binary_path}' executable"
|
||||||
with self.open_file("server.conf") as fd:
|
with self.open_file("server.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
(self.template_config).format(
|
(self.template_config).format(
|
||||||
@ -71,6 +62,7 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
|
|||||||
services_port=services_port,
|
services_port=services_port,
|
||||||
password_field=password_field,
|
password_field=password_field,
|
||||||
ssl_config=ssl_config,
|
ssl_config=ssl_config,
|
||||||
|
install_prefix=Path(binary_path).parent.parent,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert self.directory
|
assert self.directory
|
||||||
@ -81,7 +73,7 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = self.execute(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
self.binary_name,
|
self.binary_name,
|
||||||
@ -91,7 +83,6 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
|
|||||||
"-pidfile",
|
"-pidfile",
|
||||||
self.directory / "server.pid",
|
self.directory / "server.pid",
|
||||||
],
|
],
|
||||||
# stderr=subprocess.DEVNULL,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if run_services:
|
if run_services:
|
||||||
|
@ -44,7 +44,7 @@ channel {{
|
|||||||
displayed_usercount = 0;
|
displayed_usercount = 0;
|
||||||
}};
|
}};
|
||||||
|
|
||||||
connect "services.example.org" {{
|
connect "My.Little.Services" {{
|
||||||
host = "localhost"; # Used to validate incoming connection
|
host = "localhost"; # Used to validate incoming connection
|
||||||
port = 0; # We don't need servers to connect to services
|
port = 0; # We don't need servers to connect to services
|
||||||
send_password = "password";
|
send_password = "password";
|
||||||
@ -53,14 +53,14 @@ connect "services.example.org" {{
|
|||||||
flags = topicburst;
|
flags = topicburst;
|
||||||
}};
|
}};
|
||||||
service {{
|
service {{
|
||||||
name = "services.example.org";
|
name = "My.Little.Services";
|
||||||
}};
|
}};
|
||||||
|
|
||||||
privset "omnioper" {{
|
privset "omnioper" {{
|
||||||
privs = oper:general, oper:privs, oper:testline, oper:kill, oper:operwall, oper:message,
|
privs = oper:general, oper:privs, oper:testline, oper:kill, oper:operwall, oper:message,
|
||||||
oper:routing, oper:kline, oper:unkline, oper:xline,
|
oper:routing, oper:kline, oper:unkline, oper:xline,
|
||||||
oper:resv, oper:cmodes, oper:mass_notice, oper:wallops,
|
oper:resv, oper:cmodes, oper:mass_notice, oper:wallops,
|
||||||
oper:remoteban,
|
oper:remoteban, oper:local_kill,
|
||||||
usermode:servnotice, auspex:oper, auspex:hostname, auspex:umodes, auspex:cmodes,
|
usermode:servnotice, auspex:oper, auspex:hostname, auspex:umodes, auspex:cmodes,
|
||||||
oper:admin, oper:die, oper:rehash, oper:spy, oper:grant;
|
oper:admin, oper:die, oper:rehash, oper:spy, oper:grant;
|
||||||
}};
|
}};
|
||||||
|
245
irctest/controllers/dlk_services.py
Normal file
245
irctest/controllers/dlk_services.py
Normal file
@ -0,0 +1,245 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import secrets
|
||||||
|
import subprocess
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
import irctest
|
||||||
|
from irctest.basecontrollers import BaseServicesController, DirectoryBasedController
|
||||||
|
import irctest.cases
|
||||||
|
import irctest.runner
|
||||||
|
|
||||||
|
TEMPLATE_DLK_CONFIG = """\
|
||||||
|
info {{
|
||||||
|
SID "00A";
|
||||||
|
network-name "testnetwork";
|
||||||
|
services-name "My.Little.Services";
|
||||||
|
admin-email "admin@example.org";
|
||||||
|
}}
|
||||||
|
|
||||||
|
link {{
|
||||||
|
hostname "{server_hostname}";
|
||||||
|
port "{server_port}";
|
||||||
|
password "password";
|
||||||
|
}}
|
||||||
|
|
||||||
|
log {{
|
||||||
|
debug "yes";
|
||||||
|
}}
|
||||||
|
|
||||||
|
sql {{
|
||||||
|
port "3306";
|
||||||
|
username "pifpaf";
|
||||||
|
password "pifpaf";
|
||||||
|
database "pifpaf";
|
||||||
|
sockfile "{mysql_socket}";
|
||||||
|
prefix "{dlk_prefix}";
|
||||||
|
}}
|
||||||
|
|
||||||
|
wordpress {{
|
||||||
|
prefix "{wp_prefix}";
|
||||||
|
}}
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
TEMPLATE_DLK_WP_CONFIG = """
|
||||||
|
<?php
|
||||||
|
|
||||||
|
global $wpconfig;
|
||||||
|
$wpconfig = [
|
||||||
|
|
||||||
|
"dbprefix" => "{wp_prefix}",
|
||||||
|
|
||||||
|
|
||||||
|
"default_avatar" => "https://valware.uk/wp-content/plugins/ultimate-member/assets/img/default_avatar.jpg",
|
||||||
|
"forumschan" => "#DLK-Support",
|
||||||
|
|
||||||
|
];
|
||||||
|
"""
|
||||||
|
|
||||||
|
TEMPLATE_WP_CONFIG = """
|
||||||
|
define( 'DB_NAME', 'pifpaf' );
|
||||||
|
define( 'DB_USER', 'pifpaf' );
|
||||||
|
define( 'DB_PASSWORD', 'pifpaf' );
|
||||||
|
define( 'DB_HOST', 'localhost:{mysql_socket}' );
|
||||||
|
define( 'DB_CHARSET', 'utf8' );
|
||||||
|
define( 'DB_COLLATE', '' );
|
||||||
|
|
||||||
|
define( 'AUTH_KEY', 'put your unique phrase here' );
|
||||||
|
define( 'SECURE_AUTH_KEY', 'put your unique phrase here' );
|
||||||
|
define( 'LOGGED_IN_KEY', 'put your unique phrase here' );
|
||||||
|
define( 'NONCE_KEY', 'put your unique phrase here' );
|
||||||
|
define( 'AUTH_SALT', 'put your unique phrase here' );
|
||||||
|
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
|
||||||
|
define( 'LOGGED_IN_SALT', 'put your unique phrase here' );
|
||||||
|
define( 'NONCE_SALT', 'put your unique phrase here' );
|
||||||
|
|
||||||
|
$table_prefix = '{wp_prefix}';
|
||||||
|
|
||||||
|
define( 'WP_DEBUG', false );
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {{
|
||||||
|
define( 'ABSPATH', '{wp_path}' );
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* That's all, stop editing! Happy publishing. */
|
||||||
|
|
||||||
|
/** Absolute path to the WordPress directory. */
|
||||||
|
|
||||||
|
|
||||||
|
/** Sets up WordPress vars and included files. */
|
||||||
|
require_once ABSPATH . 'wp-settings.php';
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class DlkController(BaseServicesController, DirectoryBasedController):
|
||||||
|
"""Mixin for server controllers that rely on DLK"""
|
||||||
|
|
||||||
|
software_name = "Dlk-Services"
|
||||||
|
|
||||||
|
def run_sql(self, sql: str) -> None:
|
||||||
|
mysql_socket = os.environ["PIFPAF_MYSQL_SOCKET"]
|
||||||
|
subprocess.run(
|
||||||
|
["mysql", "-S", mysql_socket, "pifpaf"],
|
||||||
|
input=sql.encode(),
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
|
||||||
|
self.create_config()
|
||||||
|
|
||||||
|
if protocol == "unreal4":
|
||||||
|
protocol = "unreal5"
|
||||||
|
assert protocol in ("unreal5",), protocol
|
||||||
|
|
||||||
|
mysql_socket = os.environ["PIFPAF_MYSQL_SOCKET"]
|
||||||
|
|
||||||
|
assert self.directory
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.wp_cli_path = Path(os.environ["IRCTEST_WP_CLI_PATH"])
|
||||||
|
if not self.wp_cli_path.is_file():
|
||||||
|
raise KeyError()
|
||||||
|
except KeyError:
|
||||||
|
raise RuntimeError(
|
||||||
|
"$IRCTEST_WP_CLI_PATH must be set to a WP-CLI executable (eg. "
|
||||||
|
"downloaded from <https://raw.githubusercontent.com/wp-cli/builds/"
|
||||||
|
"gh-pages/phar/wp-cli.phar>)"
|
||||||
|
) from None
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.dlk_path = Path(os.environ["IRCTEST_DLK_PATH"])
|
||||||
|
if not self.dlk_path.is_dir():
|
||||||
|
raise KeyError()
|
||||||
|
except KeyError:
|
||||||
|
raise RuntimeError("$IRCTEST_DLK_PATH is not set") from None
|
||||||
|
self.dlk_path = self.dlk_path.resolve()
|
||||||
|
|
||||||
|
# Unpack a fresh Wordpress install in the temporary directory.
|
||||||
|
# In theory we could have a common Wordpress install and only wp-config.php
|
||||||
|
# in the temporary directory; but wp-cli assumes wp-config.php must be
|
||||||
|
# in a Wordpress directory, and fails in various places if it isn't.
|
||||||
|
# Rather than symlinking everything to make it work, let's just copy
|
||||||
|
# the whole code, it's not that big.
|
||||||
|
try:
|
||||||
|
wp_zip_path = Path(os.environ["IRCTEST_WP_ZIP_PATH"])
|
||||||
|
if not wp_zip_path.is_file():
|
||||||
|
raise KeyError()
|
||||||
|
except KeyError:
|
||||||
|
raise RuntimeError(
|
||||||
|
"$IRCTEST_WP_ZIP_PATH must be set to a Wordpress source zipball "
|
||||||
|
"(eg. downloaded from <https://wordpress.org/latest.zip>)"
|
||||||
|
) from None
|
||||||
|
subprocess.run(
|
||||||
|
["unzip", wp_zip_path, "-d", self.directory], stdout=subprocess.DEVNULL
|
||||||
|
)
|
||||||
|
self.wp_path = self.directory / "wordpress"
|
||||||
|
|
||||||
|
rand_hex = secrets.token_hex(6)
|
||||||
|
self.wp_prefix = f"wp{rand_hex}_"
|
||||||
|
self.dlk_prefix = f"dlk{rand_hex}_"
|
||||||
|
template_vars = dict(
|
||||||
|
protocol=protocol,
|
||||||
|
server_hostname=server_hostname,
|
||||||
|
server_port=server_port,
|
||||||
|
mysql_socket=mysql_socket,
|
||||||
|
wp_path=self.wp_path,
|
||||||
|
wp_prefix=self.wp_prefix,
|
||||||
|
dlk_prefix=self.dlk_prefix,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure Wordpress
|
||||||
|
wp_config_path = self.directory / "wp-config.php"
|
||||||
|
with open(wp_config_path, "w") as fd:
|
||||||
|
fd.write(TEMPLATE_WP_CONFIG.format(**template_vars))
|
||||||
|
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"php",
|
||||||
|
self.wp_cli_path,
|
||||||
|
"core",
|
||||||
|
"install",
|
||||||
|
"--url=http://localhost/",
|
||||||
|
"--title=irctest site",
|
||||||
|
"--admin_user=adminuser",
|
||||||
|
"--admin_email=adminuser@example.org",
|
||||||
|
f"--path={self.wp_path}",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configure Dlk
|
||||||
|
dlk_log_dir = self.directory / "logs"
|
||||||
|
dlk_conf_dir = self.directory / "conf"
|
||||||
|
dlk_conf_path = dlk_conf_dir / "dalek.conf"
|
||||||
|
os.mkdir(dlk_conf_dir)
|
||||||
|
with open(dlk_conf_path, "w") as fd:
|
||||||
|
fd.write(TEMPLATE_DLK_CONFIG.format(**template_vars))
|
||||||
|
dlk_wp_config_path = dlk_conf_dir / "wordpress.conf"
|
||||||
|
with open(dlk_wp_config_path, "w") as fd:
|
||||||
|
fd.write(TEMPLATE_DLK_WP_CONFIG.format(**template_vars))
|
||||||
|
(dlk_conf_dir / "modules.conf").symlink_to(self.dlk_path / "conf/modules.conf")
|
||||||
|
|
||||||
|
self.proc = self.execute(
|
||||||
|
[
|
||||||
|
"php",
|
||||||
|
"src/dalek",
|
||||||
|
],
|
||||||
|
cwd=self.dlk_path,
|
||||||
|
env={
|
||||||
|
**os.environ,
|
||||||
|
"DALEK_CONF_DIR": str(dlk_conf_dir),
|
||||||
|
"DALEK_LOG_DIR": str(dlk_log_dir),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def terminate(self) -> None:
|
||||||
|
super().terminate()
|
||||||
|
|
||||||
|
def kill(self) -> None:
|
||||||
|
super().kill()
|
||||||
|
|
||||||
|
def registerUser(
|
||||||
|
self,
|
||||||
|
case: irctest.cases.BaseServerTestCase,
|
||||||
|
username: str,
|
||||||
|
password: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
assert password
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"php",
|
||||||
|
self.wp_cli_path,
|
||||||
|
"user",
|
||||||
|
"create",
|
||||||
|
username,
|
||||||
|
f"{username}@example.org",
|
||||||
|
f"--user_pass={password}",
|
||||||
|
f"--path={self.wp_path}",
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_irctest_controller_class() -> Type[DlkController]:
|
||||||
|
return DlkController
|
@ -3,13 +3,9 @@ import json
|
|||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Any, Dict, Optional, Set, Type, Union
|
from typing import Any, Dict, Optional, Type, Union
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
||||||
BaseServerController,
|
|
||||||
DirectoryBasedController,
|
|
||||||
NotImplementedByController,
|
|
||||||
)
|
|
||||||
from irctest.cases import BaseServerTestCase
|
from irctest.cases import BaseServerTestCase
|
||||||
|
|
||||||
BASE_CONFIG = {
|
BASE_CONFIG = {
|
||||||
@ -18,6 +14,7 @@ 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,
|
||||||
@ -61,6 +58,11 @@ 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},
|
||||||
@ -130,7 +132,7 @@ def hash_password(password: Union[str, bytes]) -> str:
|
|||||||
["ergo", "genpasswd"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
["ergo", "genpasswd"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
||||||
)
|
)
|
||||||
out, _ = p.communicate(input_)
|
out, _ = p.communicate(input_)
|
||||||
return out.decode("utf-8")
|
return out.decode("utf-8").strip()
|
||||||
|
|
||||||
|
|
||||||
class ErgoController(BaseServerController, DirectoryBasedController):
|
class ErgoController(BaseServerController, DirectoryBasedController):
|
||||||
@ -153,17 +155,9 @@ class ErgoController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
invalid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
restricted_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
faketime: Optional[str],
|
faketime: Optional[str],
|
||||||
config: Optional[Any] = None,
|
config: Optional[Any] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if valid_metadata_keys or invalid_metadata_keys:
|
|
||||||
raise NotImplementedByController(
|
|
||||||
"Defining valid and invalid METADATA keys."
|
|
||||||
)
|
|
||||||
|
|
||||||
self.create_config()
|
self.create_config()
|
||||||
if config is None:
|
if config is None:
|
||||||
config = copy.deepcopy(BASE_CONFIG)
|
config = copy.deepcopy(BASE_CONFIG)
|
||||||
@ -178,6 +172,16 @@ 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)
|
||||||
|
|
||||||
@ -209,7 +213,7 @@ class ErgoController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = self.execute(
|
||||||
[*faketime_cmd, "ergo", "run", "--conf", self._config_path, "--quiet"]
|
[*faketime_cmd, "ergo", "run", "--conf", self._config_path, "--quiet"]
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -223,9 +227,6 @@ class ErgoController(BaseServerController, DirectoryBasedController):
|
|||||||
username: str,
|
username: str,
|
||||||
password: Optional[str] = None,
|
password: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
# XXX: Move this somewhere else when
|
|
||||||
# https://github.com/ircv3/ircv3-specifications/pull/152 becomes
|
|
||||||
# part of the specification
|
|
||||||
if not case.run_services:
|
if not case.run_services:
|
||||||
# Ergo does not actually need this, but other controllers do, so we
|
# Ergo does not actually need this, but other controllers do, so we
|
||||||
# are checking it here as well for tests that aren't tested with other
|
# are checking it here as well for tests that aren't tested with other
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Optional, Set, Tuple, Type
|
from typing import Optional, Tuple, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import BaseServerController
|
from irctest.basecontrollers import BaseServerController
|
||||||
|
|
||||||
@ -39,9 +39,6 @@ class ExternalServerController(BaseServerController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
invalid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
restricted_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
faketime: Optional[str],
|
faketime: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
pass
|
pass
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import subprocess
|
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from irctest import authentication, tls
|
from irctest import authentication, tls
|
||||||
@ -31,7 +30,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 = subprocess.Popen(["girc_test", "connect"] + args)
|
self.proc = self.execute(["girc_test", "connect"] + args)
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[GircController]:
|
def get_irctest_controller_class() -> Type[GircController]:
|
||||||
|
@ -3,6 +3,9 @@ from typing import Set, Type
|
|||||||
from .base_hybrid import BaseHybridController
|
from .base_hybrid import BaseHybridController
|
||||||
|
|
||||||
TEMPLATE_CONFIG = """
|
TEMPLATE_CONFIG = """
|
||||||
|
module_base_path = "{install_prefix}/lib/ircd-hybrid/modules";
|
||||||
|
.include "./reference.modules.conf"
|
||||||
|
|
||||||
serverinfo {{
|
serverinfo {{
|
||||||
name = "My.Little.Server";
|
name = "My.Little.Server";
|
||||||
sid = "42X";
|
sid = "42X";
|
||||||
@ -39,7 +42,7 @@ class {{
|
|||||||
connectfreq = 5 minutes;
|
connectfreq = 5 minutes;
|
||||||
}};
|
}};
|
||||||
connect {{
|
connect {{
|
||||||
name = "services.example.org";
|
name = "My.Little.Services";
|
||||||
host = "127.0.0.1"; # Used to validate incoming connection
|
host = "127.0.0.1"; # Used to validate incoming connection
|
||||||
port = 0; # We don't need servers to connect to services
|
port = 0; # We don't need servers to connect to services
|
||||||
send_password = "password";
|
send_password = "password";
|
||||||
@ -47,7 +50,7 @@ connect {{
|
|||||||
class = "server";
|
class = "server";
|
||||||
}};
|
}};
|
||||||
service {{
|
service {{
|
||||||
name = "services.example.org";
|
name = "My.Little.Services";
|
||||||
}};
|
}};
|
||||||
|
|
||||||
auth {{
|
auth {{
|
||||||
|
@ -1,13 +1,9 @@
|
|||||||
|
import functools
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Set, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
||||||
BaseServerController,
|
|
||||||
DirectoryBasedController,
|
|
||||||
NotImplementedByController,
|
|
||||||
)
|
|
||||||
from irctest.irc_utils.junkdrawer import find_hostname_and_port
|
|
||||||
|
|
||||||
TEMPLATE_CONFIG = """
|
TEMPLATE_CONFIG = """
|
||||||
# Clients:
|
# Clients:
|
||||||
@ -23,7 +19,7 @@ TEMPLATE_CONFIG = """
|
|||||||
|
|
||||||
<class
|
<class
|
||||||
name="ServerOperators"
|
name="ServerOperators"
|
||||||
commands="WALLOPS GLOBOPS"
|
commands="WALLOPS GLOBOPS KILL"
|
||||||
privs="channels/auspex users/auspex channels/auspex servers/auspex"
|
privs="channels/auspex users/auspex channels/auspex servers/auspex"
|
||||||
>
|
>
|
||||||
<type
|
<type
|
||||||
@ -37,14 +33,15 @@ TEMPLATE_CONFIG = """
|
|||||||
class="ServerOperators"
|
class="ServerOperators"
|
||||||
>
|
>
|
||||||
|
|
||||||
<options casemapping="ascii">
|
<options casemapping="ascii"
|
||||||
|
extbanformat="any">
|
||||||
|
|
||||||
# Disable 'NOTICE #chan :*** foo invited bar into the channel-
|
# Disable 'NOTICE #chan :*** foo invited bar into the channel-
|
||||||
<security announceinvites="none">
|
<security announceinvites="none">
|
||||||
|
|
||||||
# Services:
|
# Services:
|
||||||
<bind address="{services_hostname}" port="{services_port}" type="servers">
|
<bind address="{services_hostname}" port="{services_port}" type="servers">
|
||||||
<link name="services.example.org"
|
<link name="My.Little.Services"
|
||||||
ipaddr="{services_hostname}"
|
ipaddr="{services_hostname}"
|
||||||
port="{services_port}"
|
port="{services_port}"
|
||||||
allowmask="*"
|
allowmask="*"
|
||||||
@ -52,11 +49,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="My.Little.Services">
|
||||||
|
|
||||||
# Protocol:
|
# Protocol:
|
||||||
<module name="banexception">
|
<module name="banexception">
|
||||||
@ -75,13 +70,10 @@ TEMPLATE_CONFIG = """
|
|||||||
<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
|
||||||
# HELP/HELPOP
|
|
||||||
<module name="alias"> # for the HELP alias
|
<module name="alias"> # for the HELP alias
|
||||||
<module name="helpop">
|
{version_config}
|
||||||
<include file="examples/helpop.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">
|
||||||
@ -93,9 +85,42 @@ 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()
|
||||||
|
def installed_version() -> int:
|
||||||
|
output = subprocess.check_output(["inspircd", "--version"], universal_newlines=True)
|
||||||
|
if output.startswith("InspIRCd-3"):
|
||||||
|
return 3
|
||||||
|
if output.startswith("InspIRCd-4"):
|
||||||
|
return 4
|
||||||
|
if output.startswith("InspIRCd-5"):
|
||||||
|
return 5
|
||||||
|
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"
|
||||||
@ -113,20 +138,13 @@ class InspircdController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
invalid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
restricted_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
faketime: Optional[str] = None,
|
faketime: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if valid_metadata_keys or invalid_metadata_keys:
|
|
||||||
raise NotImplementedByController(
|
|
||||||
"Defining valid and invalid METADATA keys."
|
|
||||||
)
|
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.create_config()
|
self.create_config()
|
||||||
(services_hostname, services_port) = find_hostname_and_port()
|
(services_hostname, services_port) = self.get_hostname_and_port()
|
||||||
|
|
||||||
password_field = 'password="{}"'.format(password) if password else ""
|
password_field = 'password="{}"'.format(password) if password else ""
|
||||||
|
|
||||||
@ -138,6 +156,13 @@ class InspircdController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
ssl_config = ""
|
ssl_config = ""
|
||||||
|
|
||||||
|
if installed_version() == 3:
|
||||||
|
version_config = TEMPLATE_V3_CONFIG
|
||||||
|
elif installed_version() >= 4:
|
||||||
|
version_config = TEMPLATE_V4_CONFIG
|
||||||
|
else:
|
||||||
|
assert False, f"unexpected version: {installed_version()}"
|
||||||
|
|
||||||
with self.open_file("server.conf") as fd:
|
with self.open_file("server.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
TEMPLATE_CONFIG.format(
|
TEMPLATE_CONFIG.format(
|
||||||
@ -147,6 +172,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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert self.directory
|
assert self.directory
|
||||||
@ -157,15 +183,22 @@ class InspircdController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
extra_args = []
|
||||||
|
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,6 +1,5 @@
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
from typing import Optional, Type
|
||||||
from typing import Optional, Set, Type
|
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import (
|
||||||
BaseServerController,
|
BaseServerController,
|
||||||
@ -25,7 +24,7 @@ Y:10:90::100:512000:100.100:100.100:
|
|||||||
I::{password_field}:::10::
|
I::{password_field}:::10::
|
||||||
|
|
||||||
# O:<TARGET Host NAME>:<Password>:<Nickname>:<Port>:<Class>:<Flags>:
|
# O:<TARGET Host NAME>:<Password>:<Nickname>:<Port>:<Class>:<Flags>:
|
||||||
O:*:operpassword:operuser::::
|
O:*:operpassword:operuser:::K:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -49,14 +48,8 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
invalid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
faketime: Optional[str],
|
faketime: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
if valid_metadata_keys or invalid_metadata_keys:
|
|
||||||
raise NotImplementedByController(
|
|
||||||
"Defining valid and invalid METADATA keys."
|
|
||||||
)
|
|
||||||
if ssl:
|
if ssl:
|
||||||
raise NotImplementedByController("TLS")
|
raise NotImplementedByController("TLS")
|
||||||
if run_services:
|
if run_services:
|
||||||
@ -84,7 +77,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = self.execute(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"ircd",
|
"ircd",
|
||||||
@ -94,7 +87,6 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
"-f",
|
"-f",
|
||||||
self.directory / "server.conf",
|
self.directory / "server.conf",
|
||||||
],
|
],
|
||||||
# stderr=subprocess.DEVNULL,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
from typing import Optional, Type
|
||||||
from typing import Optional, Set, Type
|
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import (
|
||||||
BaseServerController,
|
BaseServerController,
|
||||||
@ -68,14 +67,8 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
invalid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
faketime: Optional[str],
|
faketime: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
if valid_metadata_keys or invalid_metadata_keys:
|
|
||||||
raise NotImplementedByController(
|
|
||||||
"Defining valid and invalid METADATA keys."
|
|
||||||
)
|
|
||||||
if ssl:
|
if ssl:
|
||||||
raise NotImplementedByController("TLS")
|
raise NotImplementedByController("TLS")
|
||||||
if run_services:
|
if run_services:
|
||||||
@ -103,7 +96,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = self.execute(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"ircd",
|
"ircd",
|
||||||
@ -113,7 +106,6 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
"-x",
|
"-x",
|
||||||
"DEBUG",
|
"DEBUG",
|
||||||
],
|
],
|
||||||
# stderr=subprocess.DEVNULL,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import subprocess
|
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
from irctest import authentication, tls
|
from irctest import authentication, tls
|
||||||
@ -84,7 +83,7 @@ class LimnoriaController(BaseClientController, DirectoryBasedController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert self.directory
|
assert self.directory
|
||||||
self.proc = subprocess.Popen(["supybot", self.directory / "bot.conf"])
|
self.proc = self.execute(["supybot", self.directory / "bot.conf"])
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[LimnoriaController]:
|
def get_irctest_controller_class() -> Type[LimnoriaController]:
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
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 (
|
||||||
@ -33,10 +32,10 @@ extensions:
|
|||||||
- mammon.ext.ircv3.sasl
|
- mammon.ext.ircv3.sasl
|
||||||
- mammon.ext.misc.nopost
|
- mammon.ext.misc.nopost
|
||||||
metadata:
|
metadata:
|
||||||
restricted_keys:
|
restricted_keys: []
|
||||||
{restricted_keys}
|
|
||||||
whitelist:
|
whitelist:
|
||||||
{authorized_keys}
|
- display-name
|
||||||
|
- avatar
|
||||||
monitor:
|
monitor:
|
||||||
limit: 20
|
limit: 20
|
||||||
motd:
|
motd:
|
||||||
@ -89,9 +88,6 @@ class MammonController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
invalid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
restricted_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
faketime: Optional[str],
|
faketime: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
if password is not None:
|
if password is not None:
|
||||||
@ -107,8 +103,6 @@ class MammonController(BaseServerController, DirectoryBasedController):
|
|||||||
directory=self.directory,
|
directory=self.directory,
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
port=port,
|
port=port,
|
||||||
authorized_keys=make_list(valid_metadata_keys or set()),
|
|
||||||
restricted_keys=make_list(restricted_metadata_keys or set()),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# with self.open_file('server.yml', 'r') as fd:
|
# with self.open_file('server.yml', 'r') as fd:
|
||||||
@ -121,7 +115,7 @@ class MammonController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = self.execute(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"mammond",
|
"mammond",
|
||||||
|
@ -1,13 +1,7 @@
|
|||||||
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 BaseServerController, DirectoryBasedController
|
||||||
BaseServerController,
|
|
||||||
DirectoryBasedController,
|
|
||||||
NotImplementedByController,
|
|
||||||
)
|
|
||||||
from irctest.irc_utils.junkdrawer import find_hostname_and_port
|
|
||||||
|
|
||||||
TEMPLATE_CONFIG = """
|
TEMPLATE_CONFIG = """
|
||||||
[Global]
|
[Global]
|
||||||
@ -20,7 +14,7 @@ TEMPLATE_CONFIG = """
|
|||||||
{password_field}
|
{password_field}
|
||||||
|
|
||||||
[Server]
|
[Server]
|
||||||
Name = services.example.org
|
Name = My.Little.Services
|
||||||
MyPassword = password
|
MyPassword = password
|
||||||
PeerPassword = password
|
PeerPassword = password
|
||||||
Passive = yes # don't connect to it
|
Passive = yes # don't connect to it
|
||||||
@ -28,10 +22,14 @@ 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
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -53,20 +51,13 @@ class NgircdController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
invalid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
restricted_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
faketime: Optional[str],
|
faketime: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
if valid_metadata_keys or invalid_metadata_keys:
|
|
||||||
raise NotImplementedByController(
|
|
||||||
"Defining valid and invalid METADATA keys."
|
|
||||||
)
|
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.create_config()
|
self.create_config()
|
||||||
(unused_hostname, unused_port) = find_hostname_and_port()
|
(unused_hostname, unused_port) = self.get_hostname_and_port()
|
||||||
|
|
||||||
password_field = "Password = {}".format(password) if password else ""
|
password_field = "Password = {}".format(password) if password else ""
|
||||||
|
|
||||||
@ -103,7 +94,7 @@ class NgircdController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = self.execute(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"ngircd",
|
"ngircd",
|
||||||
@ -111,7 +102,6 @@ class NgircdController(BaseServerController, DirectoryBasedController):
|
|||||||
"--config",
|
"--config",
|
||||||
self.directory / "server.conf",
|
self.directory / "server.conf",
|
||||||
],
|
],
|
||||||
# stdout=subprocess.DEVNULL,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if run_services:
|
if run_services:
|
||||||
|
@ -44,7 +44,7 @@ class {{
|
|||||||
connectfreq = 5 minutes;
|
connectfreq = 5 minutes;
|
||||||
}};
|
}};
|
||||||
connect {{
|
connect {{
|
||||||
name = "services.example.org";
|
name = "My.Little.Services";
|
||||||
host = "127.0.0.1"; # Used to validate incoming connection
|
host = "127.0.0.1"; # Used to validate incoming connection
|
||||||
port = 0; # We don't need servers to connect to services
|
port = 0; # We don't need servers to connect to services
|
||||||
send_password = "password";
|
send_password = "password";
|
||||||
@ -52,7 +52,7 @@ connect {{
|
|||||||
class = "server";
|
class = "server";
|
||||||
}};
|
}};
|
||||||
service {{
|
service {{
|
||||||
name = "services.example.org";
|
name = "My.Little.Services";
|
||||||
}};
|
}};
|
||||||
|
|
||||||
auth {{
|
auth {{
|
||||||
|
549
irctest/controllers/sable.py
Normal file
549
irctest/controllers/sable.py
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from irctest.basecontrollers import (
|
||||||
|
BaseServerController,
|
||||||
|
BaseServicesController,
|
||||||
|
DirectoryBasedController,
|
||||||
|
NotImplementedByController,
|
||||||
|
)
|
||||||
|
from irctest.cases import BaseServerTestCase
|
||||||
|
from irctest.client_mock import ClientMock
|
||||||
|
from irctest.exceptions import NoMessageException
|
||||||
|
from irctest.patma import ANYSTR
|
||||||
|
|
||||||
|
GEN_CERTS = """
|
||||||
|
mkdir -p useless_openssl_data/
|
||||||
|
|
||||||
|
cat > openssl.cnf <<EOF
|
||||||
|
[ ca ]
|
||||||
|
default_ca = CA_default # The default ca section
|
||||||
|
|
||||||
|
[ CA_default ]
|
||||||
|
new_certs_dir = useless_openssl_data/
|
||||||
|
database = useless_openssl_data/db
|
||||||
|
policy = policy_anything
|
||||||
|
serial = useless_openssl_data/serial
|
||||||
|
copy_extensions = copy
|
||||||
|
email_in_dn = no
|
||||||
|
rand_serial = no
|
||||||
|
|
||||||
|
[ policy_anything ]
|
||||||
|
countryName = optional
|
||||||
|
stateOrProvinceName = optional
|
||||||
|
localityName = optional
|
||||||
|
organizationName = optional
|
||||||
|
organizationalUnitName = optional
|
||||||
|
commonName = supplied
|
||||||
|
emailAddress = optional
|
||||||
|
|
||||||
|
[ usr_cert ]
|
||||||
|
subjectAltName=subject:copy
|
||||||
|
EOF
|
||||||
|
|
||||||
|
rm -f useless_openssl_data/db
|
||||||
|
touch useless_openssl_data/db
|
||||||
|
echo 01 > useless_openssl_data/serial
|
||||||
|
|
||||||
|
# Generate CA
|
||||||
|
openssl req -x509 -nodes -newkey rsa:2048 -batch \
|
||||||
|
-subj "/CN=Test CA" \
|
||||||
|
-outform PEM -out ca_cert.pem \
|
||||||
|
-keyout ca_cert.key
|
||||||
|
|
||||||
|
for server in $*; do
|
||||||
|
openssl genrsa -traditional \
|
||||||
|
-out $server.key \
|
||||||
|
2048
|
||||||
|
openssl req -nodes -batch -new \
|
||||||
|
-addext "subjectAltName = DNS:$server" \
|
||||||
|
-key $server.key \
|
||||||
|
-outform PEM -out server_$server.req
|
||||||
|
openssl ca -config openssl.cnf -days 3650 -md sha512 -batch \
|
||||||
|
-subj /CN=$server \
|
||||||
|
-keyfile ca_cert.key -cert ca_cert.pem \
|
||||||
|
-in server_$server.req \
|
||||||
|
-out $server.pem
|
||||||
|
openssl x509 -sha1 -in $server.pem -fingerprint -noout \
|
||||||
|
| sed "s/.*=//" | sed "s/://g" | tr '[:upper:]' '[:lower:]' > $server.pem.sha1
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -r useless_openssl_data/
|
||||||
|
"""
|
||||||
|
|
||||||
|
_certs_dir = None
|
||||||
|
|
||||||
|
|
||||||
|
def certs_dir() -> Path:
|
||||||
|
global _certs_dir
|
||||||
|
if _certs_dir is None:
|
||||||
|
certs_dir = tempfile.TemporaryDirectory()
|
||||||
|
(Path(certs_dir.name) / "gen_certs.sh").write_text(GEN_CERTS)
|
||||||
|
subprocess.run(
|
||||||
|
["bash", "gen_certs.sh", "My.Little.Server", "My.Little.Services"],
|
||||||
|
cwd=certs_dir.name,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
_certs_dir = certs_dir
|
||||||
|
return Path(_certs_dir.name)
|
||||||
|
|
||||||
|
|
||||||
|
NETWORK_CONFIG = """
|
||||||
|
{
|
||||||
|
"fanout": 1,
|
||||||
|
"ca_file": "%(certs_dir)s/ca_cert.pem",
|
||||||
|
|
||||||
|
"peers": [
|
||||||
|
{ "name": "My.Little.Services", "address": "%(services_hostname)s:%(services_port)s", "fingerprint": "%(services_cert_sha1)s" },
|
||||||
|
{ "name": "My.Little.Server", "address": "%(server1_hostname)s:%(server1_port)s", "fingerprint": "%(server1_cert_sha1)s" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
NETWORK_CONFIG_CONFIG = """
|
||||||
|
{
|
||||||
|
"object_expiry": 300,
|
||||||
|
|
||||||
|
"opers": [
|
||||||
|
{
|
||||||
|
"name": "operuser",
|
||||||
|
// echo -n "operpassword" | openssl passwd -6 -stdin
|
||||||
|
"hash": "$6$z5yA.OfGliDoi/R2$BgSsguS6bxAsPSCygDisgDw5JZuo5.88eU3Hyc7/4OaNpeKIxWGjOggeHzOl0xLiZg1vfwxXjOTFN14wG5vNI."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
"alias_users": [
|
||||||
|
%(services_alias_users)s
|
||||||
|
],
|
||||||
|
|
||||||
|
"default_roles": {
|
||||||
|
"builtin:op": [
|
||||||
|
"always_send",
|
||||||
|
"op_self", "op_grant", "voice_self", "voice_grant",
|
||||||
|
"receive_op", "receive_voice", "receive_opmod",
|
||||||
|
"topic", "kick", "set_simple_mode", "set_key",
|
||||||
|
"rename",
|
||||||
|
"ban_view", "ban_add", "ban_remove_any",
|
||||||
|
"quiet_view", "quiet_add", "quiet_remove_any",
|
||||||
|
"exempt_view", "exempt_add", "exempt_remove_any",
|
||||||
|
"invite_self", "invite_other",
|
||||||
|
"invex_view", "invex_add", "invex_remove_any"
|
||||||
|
],
|
||||||
|
"builtin:voice": [
|
||||||
|
"always_send",
|
||||||
|
"voice_self",
|
||||||
|
"receive_voice",
|
||||||
|
"ban_view", "quiet_view"
|
||||||
|
],
|
||||||
|
"builtin:all": [
|
||||||
|
"ban_view", "quiet_view"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"debug_mode": true
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
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_id": 1,
|
||||||
|
"server_name": "My.Little.Server",
|
||||||
|
|
||||||
|
"management": {
|
||||||
|
"address": "%(server1_management_hostname)s:%(server1_management_port)s",
|
||||||
|
"client_ca": "%(certs_dir)s/ca_cert.pem",
|
||||||
|
"authorised_fingerprints": [
|
||||||
|
{ "name": "user1", "fingerprint": "435bc6db9f22e84ba5d9652432154617c9509370" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
"server": {
|
||||||
|
"listeners": [
|
||||||
|
{ "address": "%(c2s_hostname)s:%(c2s_port)s" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
"event_log": {
|
||||||
|
"event_expiry": 300, // five minutes, for local testing
|
||||||
|
},
|
||||||
|
|
||||||
|
"tls_config": {
|
||||||
|
"key_file": "%(certs_dir)s/My.Little.Server.key",
|
||||||
|
"cert_file": "%(certs_dir)s/My.Little.Server.pem",
|
||||||
|
},
|
||||||
|
|
||||||
|
"node_config": {
|
||||||
|
"listen_addr": "%(server1_hostname)s:%(server1_port)s",
|
||||||
|
"cert_file": "%(certs_dir)s/My.Little.Server.pem",
|
||||||
|
"key_file": "%(certs_dir)s/My.Little.Server.key",
|
||||||
|
},
|
||||||
|
|
||||||
|
"log": {
|
||||||
|
"dir": "log/server1/",
|
||||||
|
|
||||||
|
"module-levels": {
|
||||||
|
"": "debug",
|
||||||
|
"sable_ircd": "trace",
|
||||||
|
},
|
||||||
|
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"target": "stdout",
|
||||||
|
"level": "trace",
|
||||||
|
"modules": [ "sable", "audit", "client_listener" ],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
SERVICES_CONFIG = """
|
||||||
|
{
|
||||||
|
"server_id": 99,
|
||||||
|
"server_name": "My.Little.Services",
|
||||||
|
|
||||||
|
"management": {
|
||||||
|
"address": "%(services_management_hostname)s:%(services_management_port)s",
|
||||||
|
"client_ca": "%(certs_dir)s/ca_cert.pem",
|
||||||
|
"authorised_fingerprints": [
|
||||||
|
{ "name": "user1", "fingerprint": "435bc6db9f22e84ba5d9652432154617c9509370" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
"server": {
|
||||||
|
"database": "test_database.json",
|
||||||
|
"default_roles": {
|
||||||
|
"builtin:founder": [
|
||||||
|
"founder", "access_view", "access_edit", "role_view", "role_edit",
|
||||||
|
"op_self", "op_grant",
|
||||||
|
"voice_self", "voice_grant",
|
||||||
|
"always_send",
|
||||||
|
"invite_self", "invite_other",
|
||||||
|
"receive_op", "receive_voice", "receive_opmod",
|
||||||
|
"topic", "kick", "set_simple_mode", "set_key",
|
||||||
|
"rename",
|
||||||
|
"ban_view", "ban_add", "ban_remove_any",
|
||||||
|
"quiet_view", "quiet_add", "quiet_remove_any",
|
||||||
|
"exempt_view", "exempt_add", "exempt_remove_any",
|
||||||
|
"invex_view", "invex_add", "invex_remove_any"
|
||||||
|
],
|
||||||
|
"builtin:op": [
|
||||||
|
"always_send",
|
||||||
|
"receive_op", "receive_voice", "receive_opmod",
|
||||||
|
"topic", "kick", "set_simple_mode", "set_key",
|
||||||
|
"rename",
|
||||||
|
"ban_view", "ban_add", "ban_remove_any",
|
||||||
|
"quiet_view", "quiet_add", "quiet_remove_any",
|
||||||
|
"exempt_view", "exempt_add", "exempt_remove_any",
|
||||||
|
"invex_view", "invex_add", "invex_remove_any"
|
||||||
|
],
|
||||||
|
"builtin: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_expiry": 300, // five minutes, for local testing
|
||||||
|
},
|
||||||
|
|
||||||
|
"tls_config": {
|
||||||
|
"key_file": "%(certs_dir)s/My.Little.Services.key",
|
||||||
|
"cert_file": "%(certs_dir)s/My.Little.Services.pem"
|
||||||
|
},
|
||||||
|
|
||||||
|
"node_config": {
|
||||||
|
"listen_addr": "%(services_hostname)s:%(services_port)s",
|
||||||
|
"cert_file": "%(certs_dir)s/My.Little.Services.pem",
|
||||||
|
"key_file": "%(certs_dir)s/My.Little.Services.key"
|
||||||
|
},
|
||||||
|
|
||||||
|
"log": {
|
||||||
|
"dir": "log/services/",
|
||||||
|
|
||||||
|
"module-levels": {
|
||||||
|
"": "debug"
|
||||||
|
},
|
||||||
|
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"target": "stdout",
|
||||||
|
"level": "debug",
|
||||||
|
"modules": [ "sable_services" ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class SableController(BaseServerController, DirectoryBasedController):
|
||||||
|
software_name = "Sable"
|
||||||
|
supported_sasl_mechanisms = {"PLAIN"}
|
||||||
|
sync_sleep_time = 0.1
|
||||||
|
"""Sable processes commands very quickly, but responses for commands changing the
|
||||||
|
state may be sent after later commands for messages which don't."""
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
hostname: str,
|
||||||
|
port: int,
|
||||||
|
*,
|
||||||
|
password: Optional[str],
|
||||||
|
ssl: bool,
|
||||||
|
run_services: bool,
|
||||||
|
faketime: Optional[str],
|
||||||
|
) -> None:
|
||||||
|
if password is not None:
|
||||||
|
raise NotImplementedByController("PASS command")
|
||||||
|
if 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
|
||||||
|
self.port = port
|
||||||
|
self.create_config()
|
||||||
|
|
||||||
|
assert self.directory
|
||||||
|
|
||||||
|
(self.directory / "configs").mkdir()
|
||||||
|
|
||||||
|
c2s_hostname = hostname
|
||||||
|
c2s_port = port
|
||||||
|
del hostname, port
|
||||||
|
# base controller expects this to check for NickServ presence itself
|
||||||
|
self.hostname = c2s_hostname
|
||||||
|
self.port = c2s_port
|
||||||
|
|
||||||
|
(server1_hostname, server1_port) = self.get_hostname_and_port()
|
||||||
|
(services_hostname, services_port) = self.get_hostname_and_port()
|
||||||
|
|
||||||
|
# Sable requires inbound connections to match the configured hostname,
|
||||||
|
# so we can't configure 0.0.0.0
|
||||||
|
server1_hostname = services_hostname = "127.0.0.1"
|
||||||
|
|
||||||
|
(
|
||||||
|
server1_management_hostname,
|
||||||
|
server1_management_port,
|
||||||
|
) = self.get_hostname_and_port()
|
||||||
|
(
|
||||||
|
services_management_hostname,
|
||||||
|
services_management_port,
|
||||||
|
) = self.get_hostname_and_port()
|
||||||
|
|
||||||
|
self.template_vars = dict(
|
||||||
|
certs_dir=certs_dir(),
|
||||||
|
c2s_hostname=c2s_hostname,
|
||||||
|
c2s_port=c2s_port,
|
||||||
|
server1_hostname=server1_hostname,
|
||||||
|
server1_port=server1_port,
|
||||||
|
server1_cert_sha1=(certs_dir() / "My.Little.Server.pem.sha1")
|
||||||
|
.read_text()
|
||||||
|
.strip(),
|
||||||
|
server1_management_hostname=server1_management_hostname,
|
||||||
|
server1_management_port=server1_management_port,
|
||||||
|
services_hostname=services_hostname,
|
||||||
|
services_port=services_port,
|
||||||
|
services_cert_sha1=(certs_dir() / "My.Little.Services.pem.sha1")
|
||||||
|
.read_text()
|
||||||
|
.strip(),
|
||||||
|
services_management_hostname=services_management_hostname,
|
||||||
|
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:
|
||||||
|
fd.write(NETWORK_CONFIG % self.template_vars)
|
||||||
|
with self.open_file("configs/network_config.conf") as fd:
|
||||||
|
fd.write(NETWORK_CONFIG_CONFIG % self.template_vars)
|
||||||
|
with self.open_file("configs/server1.conf") as fd:
|
||||||
|
fd.write(SERVER_CONFIG % self.template_vars)
|
||||||
|
|
||||||
|
if faketime and shutil.which("faketime"):
|
||||||
|
faketime_cmd = ["faketime", "-f", faketime]
|
||||||
|
self.faketime_enabled = True
|
||||||
|
else:
|
||||||
|
faketime_cmd = []
|
||||||
|
|
||||||
|
self.proc = self.execute(
|
||||||
|
[
|
||||||
|
*faketime_cmd,
|
||||||
|
"sable_ircd",
|
||||||
|
"--foreground",
|
||||||
|
"--server-conf",
|
||||||
|
self.directory / "configs/server1.conf",
|
||||||
|
"--network-conf",
|
||||||
|
self.directory / "configs/network.conf",
|
||||||
|
"--bootstrap-network",
|
||||||
|
self.directory / "configs/network_config.conf",
|
||||||
|
],
|
||||||
|
cwd=self.directory,
|
||||||
|
preexec_fn=os.setsid,
|
||||||
|
env={"RUST_BACKTRACE": "1", **os.environ},
|
||||||
|
)
|
||||||
|
self.pgroup_id = os.getpgid(self.proc.pid)
|
||||||
|
|
||||||
|
if run_services:
|
||||||
|
self.services_controller = SableServicesController(self.test_config, self)
|
||||||
|
self.services_controller.run(
|
||||||
|
protocol="sable",
|
||||||
|
server_hostname=services_hostname,
|
||||||
|
server_port=services_port,
|
||||||
|
)
|
||||||
|
|
||||||
|
def kill_proc(self) -> None:
|
||||||
|
os.killpg(self.pgroup_id, signal.SIGKILL)
|
||||||
|
super().kill_proc()
|
||||||
|
|
||||||
|
def registerUser(
|
||||||
|
self,
|
||||||
|
case: BaseServerTestCase, # type: ignore
|
||||||
|
username: str,
|
||||||
|
password: Optional[str] = None,
|
||||||
|
) -> None:
|
||||||
|
# XXX: Move this somewhere else when
|
||||||
|
# https://github.com/ircv3/ircv3-specifications/pull/152 becomes
|
||||||
|
# part of the specification
|
||||||
|
if not case.run_services:
|
||||||
|
raise ValueError(
|
||||||
|
"Attempted to register a nick, but `run_services` it not True."
|
||||||
|
)
|
||||||
|
assert password
|
||||||
|
client = case.addClient(show_io=True)
|
||||||
|
case.sendLine(client, "NICK " + username)
|
||||||
|
case.sendLine(client, "USER r e g :user")
|
||||||
|
while case.getRegistrationMessage(client).command != "001":
|
||||||
|
pass
|
||||||
|
case.getMessages(client)
|
||||||
|
case.sendLine(
|
||||||
|
client,
|
||||||
|
f"REGISTER * * {password}",
|
||||||
|
)
|
||||||
|
for _ in range(100):
|
||||||
|
time.sleep(0.1)
|
||||||
|
try:
|
||||||
|
msg = case.getMessage(client)
|
||||||
|
except NoMessageException:
|
||||||
|
continue
|
||||||
|
case.assertMessageMatch(
|
||||||
|
msg, command="REGISTER", params=["SUCCESS", username, ANYSTR]
|
||||||
|
)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise NoMessageException()
|
||||||
|
case.sendLine(client, "QUIT")
|
||||||
|
case.assertDisconnected(client)
|
||||||
|
|
||||||
|
|
||||||
|
class SableServicesController(BaseServicesController):
|
||||||
|
server_controller: SableController
|
||||||
|
software_name = "Sable Services"
|
||||||
|
|
||||||
|
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
|
||||||
|
assert protocol == "sable"
|
||||||
|
assert self.server_controller.directory is not None
|
||||||
|
|
||||||
|
with self.server_controller.open_file("configs/services.conf") as fd:
|
||||||
|
fd.write(SERVICES_CONFIG % self.server_controller.template_vars)
|
||||||
|
|
||||||
|
self.proc = self.execute(
|
||||||
|
[
|
||||||
|
"sable_services",
|
||||||
|
"--foreground",
|
||||||
|
"--server-conf",
|
||||||
|
self.server_controller.directory / "configs/services.conf",
|
||||||
|
"--network-conf",
|
||||||
|
self.server_controller.directory / "configs/network.conf",
|
||||||
|
],
|
||||||
|
cwd=self.server_controller.directory,
|
||||||
|
preexec_fn=os.setsid,
|
||||||
|
env={"RUST_BACKTRACE": "1", **os.environ},
|
||||||
|
)
|
||||||
|
self.pgroup_id = os.getpgid(self.proc.pid)
|
||||||
|
|
||||||
|
def kill_proc(self) -> None:
|
||||||
|
os.killpg(self.pgroup_id, signal.SIGKILL)
|
||||||
|
super().kill_proc()
|
||||||
|
|
||||||
|
def wait_for_services(self) -> None:
|
||||||
|
# by default, wait_for_services() connects a user that sends a HELP command
|
||||||
|
# to NickServ and assumes services are up when it receives a non-ERR_NOSUCHNICK
|
||||||
|
# reply.
|
||||||
|
# However, with Sable, NickServ is always up, even when services are not linked,
|
||||||
|
# so we need to check a different way. We check presence of a non-EXTERNAL
|
||||||
|
# value to the sasl capability, but LINKS would
|
||||||
|
if self.services_up:
|
||||||
|
# Don't check again if they are already available
|
||||||
|
return
|
||||||
|
self.server_controller.wait_for_port()
|
||||||
|
|
||||||
|
c = ClientMock(name="chkSvs", show_io=True)
|
||||||
|
c.connect(self.server_controller.hostname, self.server_controller.port)
|
||||||
|
c.sendLine("NICK chkSvs")
|
||||||
|
c.sendLine("USER chk chk chk chk")
|
||||||
|
time.sleep(self.server_controller.sync_sleep_time)
|
||||||
|
got_end_of_motd = False
|
||||||
|
while not got_end_of_motd:
|
||||||
|
for msg in c.getMessages(synchronize=False):
|
||||||
|
if msg.command == "PING":
|
||||||
|
# Hi Unreal
|
||||||
|
c.sendLine("PONG :" + msg.params[0])
|
||||||
|
if msg.command in ("376", "422"): # RPL_ENDOFMOTD / ERR_NOMOTD
|
||||||
|
got_end_of_motd = True
|
||||||
|
|
||||||
|
timeout = time.time() + 10
|
||||||
|
while not self.services_up:
|
||||||
|
if time.time() > timeout:
|
||||||
|
raise Exception("Timeout while waiting for services")
|
||||||
|
c.sendLine("CAP LS 302")
|
||||||
|
|
||||||
|
msgs = self.getNickServResponse(c, timeout=1)
|
||||||
|
for msg in msgs:
|
||||||
|
if msg.command == "CAP":
|
||||||
|
pass
|
||||||
|
for token in msg.params[-1].split():
|
||||||
|
if token.startswith("sasl="):
|
||||||
|
if "PLAIN" in token.removeprefix("sasl=").split(","):
|
||||||
|
# SASL PLAIN is available, so services are linked.
|
||||||
|
self.services_up = True
|
||||||
|
break
|
||||||
|
|
||||||
|
c.sendLine("QUIT")
|
||||||
|
c.getMessages()
|
||||||
|
c.disconnect()
|
||||||
|
|
||||||
|
|
||||||
|
def get_irctest_controller_class() -> Type[SableController]:
|
||||||
|
return SableController
|
@ -1,6 +1,5 @@
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
from typing import Optional, Type
|
||||||
from typing import Optional, Set, Type
|
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import (
|
||||||
BaseServerController,
|
BaseServerController,
|
||||||
@ -67,14 +66,8 @@ class SnircdController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
invalid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
faketime: Optional[str],
|
faketime: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
if valid_metadata_keys or invalid_metadata_keys:
|
|
||||||
raise NotImplementedByController(
|
|
||||||
"Defining valid and invalid METADATA keys."
|
|
||||||
)
|
|
||||||
if ssl:
|
if ssl:
|
||||||
raise NotImplementedByController("TLS")
|
raise NotImplementedByController("TLS")
|
||||||
if run_services:
|
if run_services:
|
||||||
@ -102,7 +95,7 @@ class SnircdController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = self.execute(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"ircd",
|
"ircd",
|
||||||
@ -112,7 +105,6 @@ class SnircdController(BaseServerController, DirectoryBasedController):
|
|||||||
"-x",
|
"-x",
|
||||||
"DEBUG",
|
"DEBUG",
|
||||||
],
|
],
|
||||||
# stderr=subprocess.DEVNULL,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
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
|
||||||
|
|
||||||
@ -73,7 +72,7 @@ class SopelController(BaseClientController):
|
|||||||
auth_method="auth_method = sasl" if auth else "",
|
auth_method="auth_method = sasl" if auth else "",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.proc = subprocess.Popen(["sopel", "--quiet", "-c", self.filename])
|
self.proc = self.execute(["sopel", "-c", self.filename])
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[SopelController]:
|
def get_irctest_controller_class() -> Type[SopelController]:
|
||||||
|
105
irctest/controllers/thelounge.py
Normal file
105
irctest/controllers/thelounge.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from typing import Optional, Type
|
||||||
|
|
||||||
|
from irctest import authentication, tls
|
||||||
|
from irctest.basecontrollers import (
|
||||||
|
BaseClientController,
|
||||||
|
DirectoryBasedController,
|
||||||
|
NotImplementedByController,
|
||||||
|
)
|
||||||
|
|
||||||
|
TEMPLATE_CONFIG = """
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
module.exports = {config};
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TheLoungeController(BaseClientController, DirectoryBasedController):
|
||||||
|
software_name = "TheLounge"
|
||||||
|
supported_sasl_mechanisms = {
|
||||||
|
"PLAIN",
|
||||||
|
"ECDSA-NIST256P-CHALLENGE",
|
||||||
|
"SCRAM-SHA-256",
|
||||||
|
"EXTERNAL",
|
||||||
|
}
|
||||||
|
supports_sts = True
|
||||||
|
|
||||||
|
def create_config(self) -> None:
|
||||||
|
super().create_config()
|
||||||
|
with self.open_file("bot.conf"):
|
||||||
|
pass
|
||||||
|
with self.open_file("conf/users.conf"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
hostname: str,
|
||||||
|
port: int,
|
||||||
|
auth: Optional[authentication.Authentication],
|
||||||
|
tls_config: Optional[tls.TlsConfig] = None,
|
||||||
|
) -> None:
|
||||||
|
if tls_config is None:
|
||||||
|
tls_config = tls.TlsConfig(enable=False, trusted_fingerprints=[])
|
||||||
|
if tls_config and tls_config.trusted_fingerprints:
|
||||||
|
raise NotImplementedByController("Trusted fingerprints.")
|
||||||
|
if auth and any(
|
||||||
|
mech.to_string().startswith(("SCRAM-", "ECDSA-"))
|
||||||
|
for mech in auth.mechanisms
|
||||||
|
):
|
||||||
|
raise NotImplementedByController("ecdsa")
|
||||||
|
if auth and auth.password and len(auth.password) > 300:
|
||||||
|
# https://github.com/thelounge/thelounge/pull/4480
|
||||||
|
# Note that The Lounge truncates on 300 characters, not bytes.
|
||||||
|
raise NotImplementedByController("Passwords longer than 300 chars")
|
||||||
|
# Runs a client with the config given as arguments
|
||||||
|
assert self.proc is None
|
||||||
|
self.create_config()
|
||||||
|
if auth:
|
||||||
|
mechanisms = " ".join(mech.to_string() for mech in auth.mechanisms)
|
||||||
|
if auth.ecdsa_key:
|
||||||
|
with self.open_file("ecdsa_key.pem") as fd:
|
||||||
|
fd.write(auth.ecdsa_key)
|
||||||
|
else:
|
||||||
|
mechanisms = ""
|
||||||
|
|
||||||
|
assert self.directory
|
||||||
|
with self.open_file("config.js") as fd:
|
||||||
|
fd.write(
|
||||||
|
TEMPLATE_CONFIG.format(
|
||||||
|
config=json.dumps(
|
||||||
|
dict(
|
||||||
|
public=False,
|
||||||
|
host=f"unix:{self.directory}/sock", # prevents binding
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
with self.open_file("users/testuser.json") as fd:
|
||||||
|
json.dump(
|
||||||
|
dict(
|
||||||
|
networks=[
|
||||||
|
dict(
|
||||||
|
name="testnet",
|
||||||
|
host=hostname,
|
||||||
|
port=port,
|
||||||
|
tls=tls_config.enable if tls_config else "False",
|
||||||
|
sasl=mechanisms.lower(),
|
||||||
|
saslAccount=auth.username if auth else "",
|
||||||
|
saslPassword=auth.password if auth else "",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
fd,
|
||||||
|
)
|
||||||
|
with self.open_file("users/testuser.json", "r") as fd:
|
||||||
|
print("config", json.load(fd)["networks"][0]["saslPassword"])
|
||||||
|
self.proc = self.execute(
|
||||||
|
[os.environ.get("THELOUNGE_BIN", "thelounge"), "start"],
|
||||||
|
env={**os.environ, "THELOUNGE_HOME": str(self.directory)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_irctest_controller_class() -> Type[TheLoungeController]:
|
||||||
|
return TheLoungeController
|
@ -5,14 +5,9 @@ from pathlib import Path
|
|||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import textwrap
|
import textwrap
|
||||||
from typing import Callable, ContextManager, Iterator, Optional, Set, Type
|
from typing import Callable, ContextManager, Iterator, Optional, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
||||||
BaseServerController,
|
|
||||||
DirectoryBasedController,
|
|
||||||
NotImplementedByController,
|
|
||||||
)
|
|
||||||
from irctest.irc_utils.junkdrawer import find_hostname_and_port
|
|
||||||
|
|
||||||
TEMPLATE_CONFIG = """
|
TEMPLATE_CONFIG = """
|
||||||
include "modules.default.conf";
|
include "modules.default.conf";
|
||||||
@ -69,7 +64,7 @@ listen {{
|
|||||||
options {{ serversonly; }}
|
options {{ serversonly; }}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
link services.example.org {{
|
link My.Little.Services {{
|
||||||
incoming {{
|
incoming {{
|
||||||
mask *;
|
mask *;
|
||||||
}}
|
}}
|
||||||
@ -77,11 +72,11 @@ link services.example.org {{
|
|||||||
class servers;
|
class servers;
|
||||||
}}
|
}}
|
||||||
ulines {{
|
ulines {{
|
||||||
services.example.org;
|
My.Little.Services;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
set {{
|
set {{
|
||||||
sasl-server services.example.org;
|
sasl-server My.Little.Services;
|
||||||
kline-address "example@example.org";
|
kline-address "example@example.org";
|
||||||
network-name "ExampleNET";
|
network-name "ExampleNET";
|
||||||
default-server "irc.example.org";
|
default-server "irc.example.org";
|
||||||
@ -101,7 +96,7 @@ set {{
|
|||||||
}}
|
}}
|
||||||
modes-on-join "+H 100:1d"; // Enables CHATHISTORY
|
modes-on-join "+H 100:1d"; // Enables CHATHISTORY
|
||||||
|
|
||||||
{set_extras}
|
{set_v6only}
|
||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
@ -117,13 +112,31 @@ files {{
|
|||||||
}}
|
}}
|
||||||
|
|
||||||
oper "operuser" {{
|
oper "operuser" {{
|
||||||
password = "operpassword";
|
password "operpassword";
|
||||||
mask *;
|
mask *;
|
||||||
class clients;
|
class clients;
|
||||||
operclass netadmin;
|
operclass netadmin;
|
||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
SET_V6ONLY = """
|
||||||
|
// Remove RPL_WHOISSPECIAL used to advertise security groups
|
||||||
|
whois-details {
|
||||||
|
security-groups { everyone none; self none; oper none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext-policy {
|
||||||
|
server warn; // https://www.unrealircd.org/docs/FAQ#server-requires-tls
|
||||||
|
oper warn; // https://www.unrealircd.org/docs/FAQ#oper-requires-tls
|
||||||
|
}
|
||||||
|
|
||||||
|
anti-flood {
|
||||||
|
everyone {
|
||||||
|
connect-flood 255:10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def _filelock(path: Path) -> Callable[[], ContextManager]:
|
def _filelock(path: Path) -> Callable[[], ContextManager]:
|
||||||
"""Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist"""
|
"""Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist"""
|
||||||
@ -186,15 +199,8 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
valid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
invalid_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
restricted_metadata_keys: Optional[Set[str]] = None,
|
|
||||||
faketime: Optional[str],
|
faketime: Optional[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
if valid_metadata_keys or invalid_metadata_keys:
|
|
||||||
raise NotImplementedByController(
|
|
||||||
"Defining valid and invalid METADATA keys."
|
|
||||||
)
|
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
@ -207,65 +213,55 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
|
|||||||
loadmodule "cloak_md5";
|
loadmodule "cloak_md5";
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
set_extras = textwrap.indent(
|
set_v6only = SET_V6ONLY
|
||||||
textwrap.dedent(
|
|
||||||
"""
|
|
||||||
// Remove RPL_WHOISSPECIAL used to advertise security groups
|
|
||||||
whois-details {
|
|
||||||
security-groups { everyone none; self none; oper none; }
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
),
|
|
||||||
" ",
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
extras = ""
|
extras = ""
|
||||||
set_extras = ""
|
set_v6only = ""
|
||||||
|
|
||||||
with self.open_file("empty.txt") as fd:
|
with self.open_file("empty.txt") as fd:
|
||||||
fd.write("\n")
|
fd.write("\n")
|
||||||
|
|
||||||
password_field = 'password "{}";'.format(password) if password else ""
|
password_field = 'password "{}";'.format(password) if password else ""
|
||||||
|
|
||||||
with _STARTSTOP_LOCK():
|
(services_hostname, services_port) = self.get_hostname_and_port()
|
||||||
(services_hostname, services_port) = find_hostname_and_port()
|
(unused_hostname, unused_port) = self.get_hostname_and_port()
|
||||||
(unused_hostname, unused_port) = find_hostname_and_port()
|
|
||||||
|
|
||||||
self.gen_ssl()
|
self.gen_ssl()
|
||||||
if ssl:
|
if ssl:
|
||||||
(tls_hostname, tls_port) = (hostname, port)
|
(tls_hostname, tls_port) = (hostname, port)
|
||||||
(hostname, port) = (unused_hostname, unused_port)
|
(hostname, port) = (unused_hostname, unused_port)
|
||||||
else:
|
else:
|
||||||
# Unreal refuses to start without TLS enabled
|
# Unreal refuses to start without TLS enabled
|
||||||
(tls_hostname, tls_port) = (unused_hostname, unused_port)
|
(tls_hostname, tls_port) = (unused_hostname, unused_port)
|
||||||
|
|
||||||
assert self.directory
|
assert self.directory
|
||||||
|
|
||||||
with self.open_file("unrealircd.conf") as fd:
|
with self.open_file("unrealircd.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
TEMPLATE_CONFIG.format(
|
TEMPLATE_CONFIG.format(
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
port=port,
|
port=port,
|
||||||
services_hostname=services_hostname,
|
services_hostname=services_hostname,
|
||||||
services_port=services_port,
|
services_port=services_port,
|
||||||
tls_hostname=tls_hostname,
|
tls_hostname=tls_hostname,
|
||||||
tls_port=tls_port,
|
tls_port=tls_port,
|
||||||
password_field=password_field,
|
password_field=password_field,
|
||||||
key_path=self.key_path,
|
key_path=self.key_path,
|
||||||
pem_path=self.pem_path,
|
pem_path=self.pem_path,
|
||||||
empty_file=self.directory / "empty.txt",
|
empty_file=self.directory / "empty.txt",
|
||||||
extras=extras,
|
set_v6only=set_v6only,
|
||||||
set_extras=set_extras,
|
extras=extras,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if faketime and shutil.which("faketime"):
|
if faketime and shutil.which("faketime"):
|
||||||
faketime_cmd = ["faketime", "-f", faketime]
|
faketime_cmd = ["faketime", "-f", faketime]
|
||||||
self.faketime_enabled = True
|
self.faketime_enabled = True
|
||||||
else:
|
else:
|
||||||
faketime_cmd = []
|
faketime_cmd = []
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
with _STARTSTOP_LOCK():
|
||||||
|
self.proc = self.execute(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
*faketime_cmd,
|
||||||
"unrealircd",
|
"unrealircd",
|
||||||
@ -274,7 +270,6 @@ 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()
|
||||||
|
|
||||||
|
@ -16,16 +16,22 @@ from typing import (
|
|||||||
Optional,
|
Optional,
|
||||||
Tuple,
|
Tuple,
|
||||||
TypeVar,
|
TypeVar,
|
||||||
|
Union,
|
||||||
)
|
)
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
from defusedxml.ElementTree import parse as parse_xml
|
from defusedxml.ElementTree import parse as parse_xml
|
||||||
import docutils.core
|
import docutils.core
|
||||||
|
|
||||||
|
from .shortxml import Namespace
|
||||||
|
|
||||||
NETLIFY_CHAR_BLACKLIST = frozenset('":<>|*?\r\n#')
|
NETLIFY_CHAR_BLACKLIST = frozenset('":<>|*?\r\n#')
|
||||||
"""Characters not allowed in output filenames"""
|
"""Characters not allowed in output filenames"""
|
||||||
|
|
||||||
|
|
||||||
|
HTML = Namespace("http://www.w3.org/1999/xhtml")
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class CaseResult:
|
class CaseResult:
|
||||||
module_name: str
|
module_name: str
|
||||||
@ -39,7 +45,7 @@ class CaseResult:
|
|||||||
type: Optional[str] = None
|
type: Optional[str] = None
|
||||||
message: Optional[str] = None
|
message: Optional[str] = None
|
||||||
|
|
||||||
def output_filename(self):
|
def output_filename(self) -> str:
|
||||||
test_name = self.test_name
|
test_name = self.test_name
|
||||||
if len(test_name) > 50 or set(test_name) & NETLIFY_CHAR_BLACKLIST:
|
if len(test_name) > 50 or set(test_name) & NETLIFY_CHAR_BLACKLIST:
|
||||||
# File name too long or otherwise invalid. This should be good enough:
|
# File name too long or otherwise invalid. This should be good enough:
|
||||||
@ -75,7 +81,7 @@ def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseR
|
|||||||
skipped = False
|
skipped = False
|
||||||
details = None
|
details = None
|
||||||
system_out = None
|
system_out = None
|
||||||
extra = {}
|
extra: Dict[str, str] = {}
|
||||||
for child in case:
|
for child in case:
|
||||||
if child.tag == "skipped":
|
if child.tag == "skipped":
|
||||||
success = True
|
success = True
|
||||||
@ -120,33 +126,43 @@ def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseR
|
|||||||
|
|
||||||
def rst_to_element(s: str) -> ET.Element:
|
def rst_to_element(s: str) -> ET.Element:
|
||||||
html = docutils.core.publish_parts(s, writer_name="xhtml")["html_body"]
|
html = docutils.core.publish_parts(s, writer_name="xhtml")["html_body"]
|
||||||
htmltree = ET.fromstring(html)
|
|
||||||
|
# Force the HTML namespace on all elements produced by docutils, which are
|
||||||
|
# unqualified
|
||||||
|
tree_builder = ET.TreeBuilder(
|
||||||
|
element_factory=lambda tag, attrib: ET.Element(
|
||||||
|
"{%s}%s" % (HTML.uri, tag),
|
||||||
|
{"{%s}%s" % (HTML.uri, k): v for (k, v) in attrib.items()},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
parser = ET.XMLParser(target=tree_builder)
|
||||||
|
|
||||||
|
htmltree = ET.fromstring(html, parser=parser)
|
||||||
return htmltree
|
return htmltree
|
||||||
|
|
||||||
|
|
||||||
def append_docstring(element: ET.Element, obj: object) -> None:
|
def docstring(obj: object) -> Optional[ET.Element]:
|
||||||
if obj.__doc__ is None:
|
if obj.__doc__ is None:
|
||||||
return
|
return None
|
||||||
|
|
||||||
element.append(rst_to_element(obj.__doc__))
|
return rst_to_element(obj.__doc__)
|
||||||
|
|
||||||
|
|
||||||
def build_job_html(job: str, results: List[CaseResult]) -> ET.Element:
|
def build_job_html(job: str, results: List[CaseResult]) -> ET.Element:
|
||||||
jobs = sorted({result.job for result in results})
|
jobs = sorted({result.job for result in results})
|
||||||
root = ET.Element("html")
|
|
||||||
head = ET.SubElement(root, "head")
|
|
||||||
ET.SubElement(head, "title").text = job
|
|
||||||
ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css")
|
|
||||||
|
|
||||||
body = ET.SubElement(root, "body")
|
table = build_test_table(jobs, results, "job-results test-matrix")
|
||||||
|
|
||||||
ET.SubElement(body, "h1").text = job
|
return HTML.html(
|
||||||
|
HTML.head(
|
||||||
table = build_test_table(jobs, results)
|
HTML.title(job),
|
||||||
table.set("class", "job-results test-matrix")
|
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
|
||||||
body.append(table)
|
),
|
||||||
|
HTML.body(
|
||||||
return root
|
HTML.h1(job),
|
||||||
|
table,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_module_html(
|
def build_module_html(
|
||||||
@ -154,105 +170,127 @@ def build_module_html(
|
|||||||
) -> ET.Element:
|
) -> ET.Element:
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
root = ET.Element("html")
|
table = build_test_table(jobs, results, "module-results test-matrix")
|
||||||
head = ET.SubElement(root, "head")
|
|
||||||
ET.SubElement(head, "title").text = module_name
|
|
||||||
ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css")
|
|
||||||
|
|
||||||
body = ET.SubElement(root, "body")
|
return HTML.html(
|
||||||
|
HTML.head(
|
||||||
ET.SubElement(body, "h1").text = module_name
|
HTML.title(module_name),
|
||||||
|
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
|
||||||
append_docstring(body, module)
|
),
|
||||||
|
HTML.body(
|
||||||
table = build_test_table(jobs, results)
|
HTML.h1(module_name),
|
||||||
table.set("class", "module-results test-matrix")
|
docstring(module),
|
||||||
body.append(table)
|
table,
|
||||||
|
),
|
||||||
return root
|
)
|
||||||
|
|
||||||
|
|
||||||
def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element:
|
def build_test_table(
|
||||||
|
jobs: List[str], results: List[CaseResult], class_: str
|
||||||
|
) -> ET.Element:
|
||||||
|
multiple_modules = len({r.module_name for r in results}) > 1
|
||||||
results_by_module_and_class = group_by(
|
results_by_module_and_class = group_by(
|
||||||
results, lambda r: (r.module_name, r.class_name)
|
results, lambda r: (r.module_name, r.class_name)
|
||||||
)
|
)
|
||||||
|
|
||||||
table = ET.Element("table")
|
job_row = HTML.tr(
|
||||||
|
HTML.th(), # column of case name
|
||||||
|
[HTML.th(HTML.div(HTML.span(job)), class_="job-name") for job in jobs],
|
||||||
|
)
|
||||||
|
|
||||||
job_row = ET.Element("tr")
|
rows = []
|
||||||
ET.SubElement(job_row, "th") # column of case name
|
|
||||||
for job in jobs:
|
|
||||||
cell = ET.SubElement(job_row, "th")
|
|
||||||
ET.SubElement(ET.SubElement(cell, "div"), "span").text = job
|
|
||||||
cell.set("class", "job-name")
|
|
||||||
|
|
||||||
for ((module_name, class_name), class_results) in sorted(
|
for (module_name, class_name), class_results in sorted(
|
||||||
results_by_module_and_class.items()
|
results_by_module_and_class.items()
|
||||||
):
|
):
|
||||||
|
if multiple_modules:
|
||||||
|
# if the page shows classes from various modules, use the fully-qualified
|
||||||
|
# name in order to disambiguate and be clearer (eg. show
|
||||||
|
# "irctest.server_tests.extended_join.MetadataTestCase" instead of just
|
||||||
|
# "MetadataTestCase" which looks like it's about IRCv3's METADATA spec.
|
||||||
|
qualified_class_name = f"{module_name}.{class_name}"
|
||||||
|
else:
|
||||||
|
# otherwise, it's not needed, so let's not display it
|
||||||
|
qualified_class_name = class_name
|
||||||
|
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
# Header row: class name
|
# Header row: class name
|
||||||
header_row = ET.SubElement(table, "tr")
|
row_anchor = f"{qualified_class_name}"
|
||||||
th = ET.SubElement(header_row, "th", colspan=str(len(jobs) + 1))
|
rows.append(
|
||||||
row_anchor = f"{class_name}"
|
HTML.tr(
|
||||||
section_header = ET.SubElement(
|
HTML.th(
|
||||||
ET.SubElement(th, "h2"),
|
HTML.h2(
|
||||||
"a",
|
HTML.a(
|
||||||
href=f"#{row_anchor}",
|
qualified_class_name,
|
||||||
id=row_anchor,
|
href=f"#{row_anchor}",
|
||||||
|
id=row_anchor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
docstring(getattr(module, class_name)),
|
||||||
|
colspan=str(len(jobs) + 1),
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
section_header.text = class_name
|
|
||||||
append_docstring(th, getattr(module, class_name))
|
|
||||||
|
|
||||||
# Header row: one column for each implementation
|
# Header row: one column for each implementation
|
||||||
table.append(job_row)
|
rows.append(job_row)
|
||||||
|
|
||||||
# One row for each test:
|
# One row for each test:
|
||||||
results_by_test = group_by(class_results, key=lambda r: r.test_name)
|
results_by_test = group_by(class_results, key=lambda r: r.test_name)
|
||||||
for (test_name, test_results) in sorted(results_by_test.items()):
|
for test_name, test_results in sorted(results_by_test.items()):
|
||||||
row_anchor = f"{class_name}.{test_name}"
|
row_anchor = f"{qualified_class_name}.{test_name}"
|
||||||
if len(row_anchor) >= 50:
|
if len(row_anchor) >= 50:
|
||||||
# Too long; give up on generating readable URL
|
# Too long; give up on generating readable URL
|
||||||
# TODO: only hash test parameter
|
# TODO: only hash test parameter
|
||||||
row_anchor = md5sum(row_anchor)
|
row_anchor = md5sum(row_anchor)
|
||||||
|
|
||||||
row = ET.SubElement(table, "tr", id=row_anchor)
|
doc = docstring(
|
||||||
|
getattr(getattr(module, class_name), test_name.split("[")[0])
|
||||||
cell = ET.SubElement(row, "th")
|
)
|
||||||
cell.set("class", "test-name")
|
row = HTML.tr(
|
||||||
cell_link = ET.SubElement(cell, "a", href=f"#{row_anchor}")
|
HTML.th(
|
||||||
cell_link.text = 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,
|
||||||
|
)
|
||||||
|
rows.append(row)
|
||||||
|
|
||||||
results_by_job = group_by(test_results, key=lambda r: r.job)
|
results_by_job = group_by(test_results, key=lambda r: r.job)
|
||||||
for job_name in jobs:
|
for job_name in jobs:
|
||||||
cell = ET.SubElement(row, "td")
|
|
||||||
try:
|
try:
|
||||||
(result,) = results_by_job[job_name]
|
(result,) = results_by_job[job_name]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
cell.set("class", "deselected")
|
row.append(HTML.td("d", class_="deselected"))
|
||||||
cell.text = "d"
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
text: Optional[str]
|
text: Union[str, None, ET.Element]
|
||||||
|
attrib = {}
|
||||||
|
|
||||||
if result.skipped:
|
if result.skipped:
|
||||||
cell.set("class", "skipped")
|
attrib["class"] = "skipped"
|
||||||
if result.type == "pytest.skip":
|
if result.type == "pytest.skip":
|
||||||
text = "s"
|
text = "s"
|
||||||
elif result.type == "pytest.xfail":
|
elif result.type == "pytest.xfail":
|
||||||
text = "X"
|
text = "X"
|
||||||
cell.set("class", "expected-failure")
|
attrib["class"] = "expected-failure"
|
||||||
else:
|
else:
|
||||||
text = result.type
|
text = result.type
|
||||||
elif result.success:
|
elif result.success:
|
||||||
cell.set("class", "success")
|
attrib["class"] = "success"
|
||||||
if result.type:
|
if result.type:
|
||||||
# dead code?
|
# dead code?
|
||||||
text = result.type
|
text = result.type
|
||||||
else:
|
else:
|
||||||
text = "."
|
text = "."
|
||||||
else:
|
else:
|
||||||
cell.set("class", "failure")
|
attrib["class"] = "failure"
|
||||||
if result.type:
|
if result.type:
|
||||||
# dead code?
|
# dead code?
|
||||||
text = result.type
|
text = result.type
|
||||||
@ -261,14 +299,15 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element:
|
|||||||
|
|
||||||
if result.system_out:
|
if result.system_out:
|
||||||
# There is a log file; link to it.
|
# There is a log file; link to it.
|
||||||
a = ET.SubElement(cell, "a", href=f"./{result.output_filename()}")
|
text = HTML.a(text or "?", href=f"./{result.output_filename()}")
|
||||||
a.text = text or "?"
|
|
||||||
else:
|
else:
|
||||||
cell.text = text or "?"
|
text = text or "?"
|
||||||
if result.message:
|
if result.message:
|
||||||
cell.set("title", result.message)
|
attrib["title"] = result.message
|
||||||
|
|
||||||
return table
|
row.append(HTML.td(text, attrib))
|
||||||
|
|
||||||
|
return HTML.table(*rows, class_=class_)
|
||||||
|
|
||||||
|
|
||||||
def write_html_pages(
|
def write_html_pages(
|
||||||
@ -292,7 +331,7 @@ def write_html_pages(
|
|||||||
for result in results
|
for result in results
|
||||||
)
|
)
|
||||||
assert is_client != is_server, (job, is_client, is_server)
|
assert is_client != is_server, (job, is_client, is_server)
|
||||||
if job.endswith(("-atheme", "-anope")):
|
if job.endswith(("-atheme", "-anope", "-dlk")):
|
||||||
assert is_server
|
assert is_server
|
||||||
job_categories[job] = "server-with-services"
|
job_categories[job] = "server-with-services"
|
||||||
elif is_server:
|
elif is_server:
|
||||||
@ -303,7 +342,7 @@ def write_html_pages(
|
|||||||
|
|
||||||
pages = []
|
pages = []
|
||||||
|
|
||||||
for (module_name, module_results) in sorted(results_by_module.items()):
|
for module_name, module_results in sorted(results_by_module.items()):
|
||||||
# Filter out client jobs if this is a server test module, and vice versa
|
# Filter out client jobs if this is a server test module, and vice versa
|
||||||
module_categories = {
|
module_categories = {
|
||||||
job_categories[result.job]
|
job_categories[result.job]
|
||||||
@ -344,18 +383,9 @@ def write_test_outputs(output_dir: Path, results: List[CaseResult]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> None:
|
def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> None:
|
||||||
root = ET.Element("html")
|
|
||||||
head = ET.SubElement(root, "head")
|
|
||||||
ET.SubElement(head, "title").text = "irctest dashboard"
|
|
||||||
ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css")
|
|
||||||
|
|
||||||
body = ET.SubElement(root, "body")
|
|
||||||
|
|
||||||
ET.SubElement(body, "h1").text = "irctest dashboard"
|
|
||||||
|
|
||||||
module_pages = []
|
module_pages = []
|
||||||
job_pages = []
|
job_pages = []
|
||||||
for (page_type, title, file_name) in sorted(pages):
|
for page_type, title, file_name in sorted(pages):
|
||||||
if page_type == "module":
|
if page_type == "module":
|
||||||
module_pages.append((title, file_name))
|
module_pages.append((title, file_name))
|
||||||
elif page_type == "job":
|
elif page_type == "job":
|
||||||
@ -363,28 +393,36 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non
|
|||||||
else:
|
else:
|
||||||
assert False, page_type
|
assert False, page_type
|
||||||
|
|
||||||
ET.SubElement(body, "h2").text = "Tests by command/specification"
|
page = HTML.html(
|
||||||
|
HTML.head(
|
||||||
|
HTML.title("irctest dashboard"),
|
||||||
|
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
|
||||||
|
),
|
||||||
|
HTML.body(
|
||||||
|
HTML.h1("irctest dashboard"),
|
||||||
|
HTML.h2("Tests by command/specification"),
|
||||||
|
HTML.dl(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
HTML.dt(HTML.a(module_name, href=f"./{file_name}")),
|
||||||
|
HTML.dd(docstring(importlib.import_module(module_name))),
|
||||||
|
)
|
||||||
|
for module_name, file_name in sorted(module_pages)
|
||||||
|
],
|
||||||
|
class_="module-index",
|
||||||
|
),
|
||||||
|
HTML.h2("Tests by implementation"),
|
||||||
|
HTML.ul(
|
||||||
|
[
|
||||||
|
HTML.li(HTML.a(job, href=f"./{file_name}"))
|
||||||
|
for job, file_name in sorted(job_pages)
|
||||||
|
],
|
||||||
|
class_="job-index",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
dl = ET.SubElement(body, "dl")
|
write_xml_file(output_dir / "index.xhtml", page)
|
||||||
dl.set("class", "module-index")
|
|
||||||
|
|
||||||
for (module_name, file_name) in sorted(module_pages):
|
|
||||||
module = importlib.import_module(module_name)
|
|
||||||
|
|
||||||
link = ET.SubElement(ET.SubElement(dl, "dt"), "a", href=f"./{file_name}")
|
|
||||||
link.text = module_name
|
|
||||||
append_docstring(ET.SubElement(dl, "dd"), module)
|
|
||||||
|
|
||||||
ET.SubElement(body, "h2").text = "Tests by implementation"
|
|
||||||
|
|
||||||
ul = ET.SubElement(body, "ul")
|
|
||||||
ul.set("class", "job-index")
|
|
||||||
|
|
||||||
for (job, file_name) in sorted(job_pages):
|
|
||||||
link = ET.SubElement(ET.SubElement(ul, "li"), "a", href=f"./{file_name}")
|
|
||||||
link.text = job
|
|
||||||
|
|
||||||
write_xml_file(output_dir / "index.xhtml", root)
|
|
||||||
|
|
||||||
|
|
||||||
def write_assets(output_dir: Path) -> None:
|
def write_assets(output_dir: Path) -> None:
|
||||||
@ -396,12 +434,12 @@ def write_assets(output_dir: Path) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def write_xml_file(filename: Path, root: ET.Element) -> None:
|
def write_xml_file(filename: Path, root: ET.Element) -> None:
|
||||||
# Hacky: ET expects the namespace to be present in every tag we create instead;
|
|
||||||
# but it would be excessively verbose.
|
|
||||||
root.set("xmlns", "http://www.w3.org/1999/xhtml")
|
|
||||||
|
|
||||||
# Serialize
|
# Serialize
|
||||||
s = ET.tostring(root)
|
if sys.version_info >= (3, 8):
|
||||||
|
s = ET.tostring(root, default_namespace=HTML.uri)
|
||||||
|
else:
|
||||||
|
# default_namespace not supported
|
||||||
|
s = ET.tostring(root)
|
||||||
|
|
||||||
with filename.open("wb") as fd:
|
with filename.open("wb") as fd:
|
||||||
fd.write(s)
|
fd.write(s)
|
||||||
|
@ -18,7 +18,7 @@ class Artifact:
|
|||||||
download_url: str
|
download_url: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_download_url(self):
|
def public_download_url(self) -> str:
|
||||||
# GitHub API is not available publicly for artifacts, we need to use
|
# GitHub API is not available publicly for artifacts, we need to use
|
||||||
# a third-party proxy to access it...
|
# a third-party proxy to access it...
|
||||||
name = urllib.parse.quote(self.name)
|
name = urllib.parse.quote(self.name)
|
||||||
|
126
irctest/dashboard/shortxml.py
Normal file
126
irctest/dashboard/shortxml.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Copyright (c) 2023 Valentin Lorentz
|
||||||
|
#
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documentation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furnished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in all
|
||||||
|
# copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
# SOFTWARE.
|
||||||
|
|
||||||
|
"""This module allows writing XML ASTs in a way that is more concise than the default
|
||||||
|
:mod:`xml.etree.ElementTree` interface.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
from .shortxml import Namespace
|
||||||
|
|
||||||
|
HTML = Namespace("http://www.w3.org/1999/xhtml")
|
||||||
|
|
||||||
|
page = HTML.html(
|
||||||
|
HTML.head(
|
||||||
|
HTML.title("irctest dashboard"),
|
||||||
|
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
|
||||||
|
),
|
||||||
|
HTML.body(
|
||||||
|
HTML.h1("irctest dashboard"),
|
||||||
|
HTML.h2("Tests by command/specification"),
|
||||||
|
HTML.dl(
|
||||||
|
[
|
||||||
|
( # elements can be arbitrarily nested in lists
|
||||||
|
HTML.dt(HTML.a(title, href=f"./{title}.xhtml")),
|
||||||
|
HTML.dd(defintion),
|
||||||
|
)
|
||||||
|
for title, definition in sorted(definitions)
|
||||||
|
],
|
||||||
|
class_="module-index",
|
||||||
|
),
|
||||||
|
HTML.h2("Tests by implementation"),
|
||||||
|
HTML.ul(
|
||||||
|
[
|
||||||
|
HTML.li(HTML.a(job, href=f"./{file_name}"))
|
||||||
|
for job, file_name in sorted(job_pages)
|
||||||
|
],
|
||||||
|
class_="job-index",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
print(ET.tostring(page, default_namespace=HTML.uri))
|
||||||
|
|
||||||
|
|
||||||
|
Attributes can be passed either as dictionaries or as kwargs, and can be mixed
|
||||||
|
with child elements.
|
||||||
|
Trailing underscores are stripped from attributes, which allows passing reserved
|
||||||
|
Python keywords (eg. ``class_`` instead of ``class``)
|
||||||
|
|
||||||
|
Attributes are always qualified, and share the namespace of the element they are
|
||||||
|
attached to.
|
||||||
|
|
||||||
|
Mixed content (elements containing both text and child elements) is not supported.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Dict, Sequence, Union
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
|
||||||
|
def _namespacify(ns: str, s: str) -> str:
|
||||||
|
return "{%s}%s" % (ns, s)
|
||||||
|
|
||||||
|
|
||||||
|
_Children = Union[None, Dict[str, str], ET.Element, Sequence["_Children"]]
|
||||||
|
|
||||||
|
|
||||||
|
class ElementFactory:
|
||||||
|
def __init__(self, namespace: str, tag: str):
|
||||||
|
self._tag = _namespacify(namespace, tag)
|
||||||
|
self._namespace = namespace
|
||||||
|
|
||||||
|
def __call__(self, *args: Union[str, _Children], **kwargs: str) -> ET.Element:
|
||||||
|
e = ET.Element(self._tag)
|
||||||
|
|
||||||
|
attributes = {k.rstrip("_"): v for (k, v) in kwargs.items()}
|
||||||
|
children = [*args, attributes]
|
||||||
|
|
||||||
|
if args and isinstance(children[0], str):
|
||||||
|
e.text = children[0]
|
||||||
|
children.pop(0)
|
||||||
|
|
||||||
|
for child in children:
|
||||||
|
self._append_child(e, child)
|
||||||
|
|
||||||
|
return e
|
||||||
|
|
||||||
|
def _append_child(self, e: ET.Element, child: _Children) -> None:
|
||||||
|
if isinstance(child, ET.Element):
|
||||||
|
e.append(child)
|
||||||
|
elif child is None:
|
||||||
|
pass
|
||||||
|
elif isinstance(child, dict):
|
||||||
|
for k, v in child.items():
|
||||||
|
e.set(_namespacify(self._namespace, k), str(v))
|
||||||
|
elif isinstance(child, str):
|
||||||
|
raise ValueError("Mixed content is not supported")
|
||||||
|
else:
|
||||||
|
for grandchild in child:
|
||||||
|
self._append_child(e, grandchild)
|
||||||
|
|
||||||
|
|
||||||
|
class Namespace:
|
||||||
|
def __init__(self, uri: str):
|
||||||
|
self.uri = uri
|
||||||
|
|
||||||
|
def __getattr__(self, tag: str) -> ElementFactory:
|
||||||
|
return ElementFactory(self.uri, tag)
|
@ -1,23 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
18
irctest/irc_utils/filelock.py
Normal file
18
irctest/irc_utils/filelock.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
Compatibility layer for filelock ( https://pypi.org/project/filelock/ );
|
||||||
|
commonly packaged by Linux distributions but might not be available
|
||||||
|
in some environments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import os
|
||||||
|
from typing import Any, ContextManager
|
||||||
|
|
||||||
|
if os.getenv("PYTEST_XDIST_WORKER"):
|
||||||
|
# running under pytest-xdist; filelock is required for reliability
|
||||||
|
from filelock import FileLock
|
||||||
|
else:
|
||||||
|
# normal test execution, no port races
|
||||||
|
|
||||||
|
def FileLock(*args: Any, **kwargs: Any) -> ContextManager[None]:
|
||||||
|
return contextlib.nullcontext()
|
@ -13,7 +13,7 @@ def ircv3_timestamp_to_unixtime(timestamp: str) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def random_name(base: str) -> str:
|
def random_name(base: str) -> str:
|
||||||
return base + "-" + secrets.token_hex(8)
|
return base + "-" + secrets.token_hex(5)
|
||||||
|
|
||||||
|
|
||||||
def find_hostname_and_port() -> Tuple[str, int]:
|
def find_hostname_and_port() -> Tuple[str, int]:
|
||||||
|
@ -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]]:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
"""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
|
||||||
|
|
||||||
@ -27,6 +28,14 @@ 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
|
||||||
@ -97,10 +106,15 @@ 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():
|
||||||
@ -128,11 +142,19 @@ 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)
|
||||||
|
|
||||||
if len(got) != len(expected):
|
nb_optionals = 0
|
||||||
|
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 zip(got, expected)
|
for (got_value, expected_value) in itertools.zip_longest(got, expected)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -152,7 +174,7 @@ def match_dict(
|
|||||||
# Set to not-None if we find a Keys() operator in the dict keys
|
# Set to not-None if we find a Keys() operator in the dict keys
|
||||||
remaining_keys_wildcard = None
|
remaining_keys_wildcard = None
|
||||||
|
|
||||||
for (expected_key, expected_value) in expected.items():
|
for expected_key, expected_value in expected.items():
|
||||||
if isinstance(expected_key, RemainingKeys):
|
if isinstance(expected_key, RemainingKeys):
|
||||||
remaining_keys_wildcard = (expected_key.key, expected_value)
|
remaining_keys_wildcard = (expected_key.key, expected_value)
|
||||||
else:
|
else:
|
||||||
@ -168,7 +190,7 @@ def match_dict(
|
|||||||
|
|
||||||
if remaining_keys_wildcard:
|
if remaining_keys_wildcard:
|
||||||
(expected_key, expected_value) = remaining_keys_wildcard
|
(expected_key, expected_value) = remaining_keys_wildcard
|
||||||
for (key, value) in got.items():
|
for key, value in got.items():
|
||||||
if not match_string(key, expected_key):
|
if not match_string(key, expected_key):
|
||||||
return False
|
return False
|
||||||
if not match_string(value, expected_value):
|
if not match_string(value, expected_value):
|
||||||
|
@ -13,6 +13,7 @@ from irctest.patma import (
|
|||||||
ANYSTR,
|
ANYSTR,
|
||||||
ListRemainder,
|
ListRemainder,
|
||||||
NotStrRe,
|
NotStrRe,
|
||||||
|
OptStrRe,
|
||||||
RemainingKeys,
|
RemainingKeys,
|
||||||
StrRe,
|
StrRe,
|
||||||
)
|
)
|
||||||
@ -172,7 +173,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 be PRIVMSG, got PRIVMG",
|
"expected command to match 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']",
|
||||||
@ -205,7 +206,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 be PRIVMSG, got PRIVMG",
|
"expected command to match 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']",
|
||||||
@ -234,12 +235,34 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
],
|
],
|
||||||
# and they each error with:
|
# and they each error with:
|
||||||
[
|
[
|
||||||
"expected command to be PRIVMSG, got PRIVMG",
|
"expected command to match 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(
|
||||||
@ -322,7 +345,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 be PING, got PONG"
|
"expected command to match PING, got PONG"
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
@ -9,14 +9,75 @@ 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_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(
|
||||||
ergo_config=lambda config: config["accounts"]["registration"].update(
|
account_registration_requires_email=False,
|
||||||
{"allow-before-connect": True}
|
account_registration_before_connect=True,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def testBeforeConnect(self):
|
def testBeforeConnect(self):
|
||||||
@ -25,7 +86,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])
|
self.assertIn("before-connect", caps[REGISTER_CAP_NAME] or "")
|
||||||
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")
|
||||||
@ -33,14 +94,14 @@ class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertMessageMatch(register_response, params=["SUCCESS", ANYSTR, ANYSTR])
|
self.assertMessageMatch(register_response, params=["SUCCESS", ANYSTR, ANYSTR])
|
||||||
|
|
||||||
|
|
||||||
|
@cases.mark_services
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
|
class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def config() -> cases.TestCaseControllerConfig:
|
def config() -> cases.TestCaseControllerConfig:
|
||||||
return cases.TestCaseControllerConfig(
|
return cases.TestCaseControllerConfig(
|
||||||
ergo_config=lambda config: config["accounts"]["registration"].update(
|
account_registration_requires_email=False,
|
||||||
{"allow-before-connect": False}
|
account_registration_before_connect=False,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def testBeforeConnect(self):
|
def testBeforeConnect(self):
|
||||||
@ -49,7 +110,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.assertEqual(caps[REGISTER_CAP_NAME], None)
|
self.assertNotIn("before-connect", caps[REGISTER_CAP_NAME] or "")
|
||||||
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")
|
||||||
@ -60,22 +121,14 @@ class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cases.mark_services
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
|
class RegisterEmailVerifiedBeforeConnectTestCase(cases.BaseServerTestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def config() -> cases.TestCaseControllerConfig:
|
def config() -> cases.TestCaseControllerConfig:
|
||||||
return cases.TestCaseControllerConfig(
|
return cases.TestCaseControllerConfig(
|
||||||
ergo_config=lambda config: config["accounts"]["registration"].update(
|
account_registration_requires_email=True,
|
||||||
{
|
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):
|
||||||
@ -86,10 +139,8 @@ class RegisterEmailVerifiedTestCase(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.assertEqual(
|
self.assertIn("email-required", caps[REGISTER_CAP_NAME] or "")
|
||||||
set(caps[REGISTER_CAP_NAME].split(",")),
|
self.assertIn("before-connect", caps[REGISTER_CAP_NAME] or "")
|
||||||
{"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")
|
||||||
@ -98,10 +149,25 @@ class RegisterEmailVerifiedTestCase(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]
|
||||||
@ -110,14 +176,14 @@ class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@cases.mark_services
|
||||||
@cases.mark_specifications("IRCv3", "Ergo")
|
@cases.mark_specifications("IRCv3", "Ergo")
|
||||||
class RegisterNoLandGrabsTestCase(cases.BaseServerTestCase):
|
class RegisterNoLandGrabsTestCase(cases.BaseServerTestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def config() -> cases.TestCaseControllerConfig:
|
def config() -> cases.TestCaseControllerConfig:
|
||||||
return cases.TestCaseControllerConfig(
|
return cases.TestCaseControllerConfig(
|
||||||
ergo_config=lambda config: config["accounts"]["registration"].update(
|
account_registration_requires_email=False,
|
||||||
{"allow-before-connect": True}
|
account_registration_before_connect=True,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def testBeforeConnect(self):
|
def testBeforeConnect(self):
|
||||||
|
@ -11,13 +11,14 @@ from irctest.numerics import (
|
|||||||
RPL_USERHOST,
|
RPL_USERHOST,
|
||||||
RPL_WHOISUSER,
|
RPL_WHOISUSER,
|
||||||
)
|
)
|
||||||
from irctest.patma import StrRe
|
from irctest.patma import ANYSTR, StrRe
|
||||||
|
|
||||||
|
|
||||||
class AwayTestCase(cases.BaseServerTestCase):
|
class AwayTestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_specifications("RFC2812", "Modern")
|
@cases.mark_specifications("RFC2812", "Modern")
|
||||||
def testAway(self):
|
def testAway(self):
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
|
self.getMessages(1)
|
||||||
self.sendLine(1, "AWAY :I'm not here right now")
|
self.sendLine(1, "AWAY :I'm not here right now")
|
||||||
replies = self.getMessages(1)
|
replies = self.getMessages(1)
|
||||||
self.assertIn(RPL_NOWAWAY, [msg.command for msg in replies])
|
self.assertIn(RPL_NOWAWAY, [msg.command for msg in replies])
|
||||||
@ -29,6 +30,7 @@ class AwayTestCase(cases.BaseServerTestCase):
|
|||||||
command=RPL_AWAY,
|
command=RPL_AWAY,
|
||||||
params=["qux", "bar", "I'm not here right now"],
|
params=["qux", "bar", "I'm not here right now"],
|
||||||
)
|
)
|
||||||
|
self.getMessages(1)
|
||||||
|
|
||||||
self.sendLine(1, "AWAY")
|
self.sendLine(1, "AWAY")
|
||||||
replies = self.getMessages(1)
|
replies = self.getMessages(1)
|
||||||
@ -47,12 +49,16 @@ class AwayTestCase(cases.BaseServerTestCase):
|
|||||||
"""
|
"""
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
self.sendLine(1, "AWAY :I'm not here right now")
|
self.sendLine(1, "AWAY :I'm not here right now")
|
||||||
replies = self.getMessages(1)
|
self.assertMessageMatch(
|
||||||
self.assertIn(RPL_NOWAWAY, [msg.command for msg in replies])
|
self.getMessage(1), command=RPL_NOWAWAY, params=["bar", ANYSTR]
|
||||||
|
)
|
||||||
|
self.assertEqual(self.getMessages(1), [])
|
||||||
|
|
||||||
self.sendLine(1, "AWAY")
|
self.sendLine(1, "AWAY")
|
||||||
replies = self.getMessages(1)
|
self.assertMessageMatch(
|
||||||
self.assertIn(RPL_UNAWAY, [msg.command for msg in replies])
|
self.getMessage(1), command=RPL_UNAWAY, params=["bar", ANYSTR]
|
||||||
|
)
|
||||||
|
self.assertEqual(self.getMessages(1), [])
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testAwayPrivmsg(self):
|
def testAwayPrivmsg(self):
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
|
from irctest.numerics import RPL_NOWAWAY, RPL_UNAWAY
|
||||||
|
from irctest.patma import ANYSTR, StrRe
|
||||||
|
|
||||||
|
|
||||||
class AwayNotifyTestCase(cases.BaseServerTestCase):
|
class AwayNotifyTestCase(cases.BaseServerTestCase):
|
||||||
@ -20,13 +22,28 @@ class AwayNotifyTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
|
|
||||||
self.sendLine(2, "AWAY :i'm going away")
|
self.sendLine(2, "AWAY :i'm going away")
|
||||||
self.getMessages(2)
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(2), command=RPL_NOWAWAY, params=["bar", ANYSTR]
|
||||||
|
)
|
||||||
|
self.assertEqual(self.getMessages(2), [])
|
||||||
|
|
||||||
awayNotify = self.getMessage(1)
|
awayNotify = self.getMessage(1)
|
||||||
self.assertMessageMatch(awayNotify, command="AWAY", params=["i'm going away"])
|
self.assertMessageMatch(
|
||||||
self.assertTrue(
|
awayNotify,
|
||||||
awayNotify.prefix.startswith("bar!"),
|
prefix=StrRe("bar!.*"),
|
||||||
"Unexpected away-notify source: %s" % (awayNotify.prefix,),
|
command="AWAY",
|
||||||
|
params=["i'm going away"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(2, "AWAY")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(2), command=RPL_UNAWAY, params=["bar", ANYSTR]
|
||||||
|
)
|
||||||
|
self.assertEqual(self.getMessages(2), [])
|
||||||
|
|
||||||
|
awayNotify = self.getMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
awayNotify, prefix=StrRe("bar!.*"), command="AWAY", params=[]
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_capabilities("away-notify")
|
@cases.mark_capabilities("away-notify")
|
||||||
@ -45,7 +62,11 @@ class AwayNotifyTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
|
|
||||||
self.joinChannel(2, "#chan")
|
self.joinChannel(2, "#chan")
|
||||||
self.getMessages(2)
|
self.assertNotIn(
|
||||||
|
"AWAY",
|
||||||
|
[m.command for m in self.getMessages(2)],
|
||||||
|
"joining user got their own away status when they joined",
|
||||||
|
)
|
||||||
|
|
||||||
messages = [msg for msg in self.getMessages(1) if msg.command == "AWAY"]
|
messages = [msg for msg in self.getMessages(1) if msg.command == "AWAY"]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -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"),
|
len((line + payload + "\r\n").encode()),
|
||||||
512 - overhead,
|
512 - overhead,
|
||||||
"Got ERR_INPUTTOOLONG for a messag that should fit "
|
"Got ERR_INPUTTOOLONG for a message that should fit "
|
||||||
"withing 512 characters.",
|
"within 512 characters.",
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -125,11 +125,24 @@ 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):
|
||||||
self.sendLine(client1, f"PRIVMSG nick2 {colon}a\r\n")
|
"""Compute the overhead added to client1's message:
|
||||||
|
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(f"PRIVMSG nick2 {colon}a\r\n")
|
return len(line) - len(outgoing.encode())
|
||||||
|
|
||||||
def _getLine(self, client) -> bytes:
|
def _getLine(self, client) -> bytes:
|
||||||
line = b""
|
line = b""
|
||||||
|
@ -4,11 +4,32 @@
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.patma import ANYSTR
|
from irctest.patma import ANYSTR, StrRe
|
||||||
from irctest.runner import CapabilityNotSupported, ImplementationChoice
|
from irctest.runner import CapabilityNotSupported, ImplementationChoice
|
||||||
|
|
||||||
|
|
||||||
class CapTestCase(cases.BaseServerTestCase):
|
class CapTestCase(cases.BaseServerTestCase):
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
def testInvalidCapSubcommand(self):
|
||||||
|
"""“If no capabilities are active, an empty parameter must be sent.”
|
||||||
|
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-list-subcommand>
|
||||||
|
""" # noqa
|
||||||
|
self.addClient()
|
||||||
|
self.sendLine(1, "CAP NOTACOMMAND")
|
||||||
|
self.sendLine(1, "PING test123")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertTrue(
|
||||||
|
self.messageDiffers(m, command="PONG", params=[ANYSTR, "test123"]),
|
||||||
|
"Sending “CAP NOTACOMMAND” as first message got no reply",
|
||||||
|
)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="410",
|
||||||
|
params=["*", "NOTACOMMAND", ANYSTR],
|
||||||
|
fail_msg="Sending “CAP NOTACOMMAND” as first message got a reply "
|
||||||
|
"that is not ERR_INVALIDCAPCMD: {msg}",
|
||||||
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
def testNoReq(self):
|
def testNoReq(self):
|
||||||
"""Test the server handles gracefully clients which do not send
|
"""Test the server handles gracefully clients which do not send
|
||||||
@ -23,12 +44,206 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
self.getCapLs(1)
|
self.getCapLs(1)
|
||||||
self.sendLine(1, "USER foo foo foo :foo")
|
self.sendLine(1, "USER foo foo foo :foo")
|
||||||
self.sendLine(1, "NICK foo")
|
self.sendLine(1, "NICK foo")
|
||||||
|
|
||||||
|
# Make sure the server didn't send anything yet
|
||||||
|
self.sendLine(1, "CAP LS 302")
|
||||||
|
self.getCapLs(1)
|
||||||
|
|
||||||
self.sendLine(1, "CAP END")
|
self.sendLine(1, "CAP END")
|
||||||
m = self.getRegistrationMessage(1)
|
m = self.getRegistrationMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
def testReqOne(self):
|
||||||
|
"""Tests requesting a single capability"""
|
||||||
|
self.addClient(1)
|
||||||
|
self.sendLine(1, "CAP LS")
|
||||||
|
self.getCapLs(1)
|
||||||
|
self.sendLine(1, "USER foo foo foo :foo")
|
||||||
|
self.sendLine(1, "NICK foo")
|
||||||
|
self.sendLine(1, "CAP REQ :multi-prefix")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
|
||||||
|
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP LIST")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=[ANYSTR, "LIST", StrRe("multi-prefix ?")],
|
||||||
|
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP END")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
||||||
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.xfailIfSoftware(
|
||||||
|
["ngIRCd"],
|
||||||
|
"does not support userhost-in-names",
|
||||||
|
)
|
||||||
|
def testReqTwo(self):
|
||||||
|
"""Tests requesting two capabilities at once"""
|
||||||
|
self.addClient(1)
|
||||||
|
self.sendLine(1, "CAP LS")
|
||||||
|
self.getCapLs(1)
|
||||||
|
self.sendLine(1, "USER foo foo foo :foo")
|
||||||
|
self.sendLine(1, "NICK foo")
|
||||||
|
self.sendLine(1, "CAP REQ :multi-prefix userhost-in-names")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=[ANYSTR, "ACK", StrRe("multi-prefix userhost-in-names ?")],
|
||||||
|
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP LIST")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=[
|
||||||
|
ANYSTR,
|
||||||
|
"LIST",
|
||||||
|
StrRe(
|
||||||
|
"(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP END")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
||||||
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.xfailIfSoftware(
|
||||||
|
["ngIRCd"],
|
||||||
|
"does not support userhost-in-names",
|
||||||
|
)
|
||||||
|
def testReqOneThenOne(self):
|
||||||
|
"""Tests requesting two capabilities in different messages"""
|
||||||
|
self.addClient(1)
|
||||||
|
self.sendLine(1, "CAP LS")
|
||||||
|
self.getCapLs(1)
|
||||||
|
self.sendLine(1, "USER foo foo foo :foo")
|
||||||
|
self.sendLine(1, "NICK foo")
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP REQ :multi-prefix")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
|
||||||
|
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP REQ :userhost-in-names")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=[ANYSTR, "ACK", StrRe("userhost-in-names ?")],
|
||||||
|
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP LIST")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=[
|
||||||
|
ANYSTR,
|
||||||
|
"LIST",
|
||||||
|
StrRe(
|
||||||
|
"(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP END")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
||||||
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.xfailIfSoftware(
|
||||||
|
["ngIRCd"],
|
||||||
|
"does not support userhost-in-names",
|
||||||
|
)
|
||||||
|
def testReqPostRegistration(self):
|
||||||
|
"""Tests requesting more capabilities after CAP END"""
|
||||||
|
self.addClient(1)
|
||||||
|
self.sendLine(1, "CAP LS")
|
||||||
|
self.getCapLs(1)
|
||||||
|
self.sendLine(1, "USER foo foo foo :foo")
|
||||||
|
self.sendLine(1, "NICK foo")
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP REQ :multi-prefix")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
|
||||||
|
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP LIST")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=[ANYSTR, "LIST", StrRe("multi-prefix ?")],
|
||||||
|
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP END")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
||||||
|
)
|
||||||
|
|
||||||
|
self.getMessages(1)
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP REQ :userhost-in-names")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=[ANYSTR, "ACK", StrRe("userhost-in-names ?")],
|
||||||
|
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "CAP LIST")
|
||||||
|
m = self.getMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=[
|
||||||
|
ANYSTR,
|
||||||
|
"LIST",
|
||||||
|
StrRe(
|
||||||
|
"(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
|
||||||
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
def testReqUnavailable(self):
|
def testReqUnavailable(self):
|
||||||
"""Test the server handles gracefully clients which request
|
"""Test the server handles gracefully clients which request
|
||||||
@ -45,7 +260,7 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
command="CAP",
|
command="CAP",
|
||||||
params=[ANYSTR, "NAK", "foo"],
|
params=[ANYSTR, "NAK", StrRe("foo ?")],
|
||||||
fail_msg="Expected CAP NAK after requesting non-existing "
|
fail_msg="Expected CAP NAK after requesting non-existing "
|
||||||
"capability, got {msg}.",
|
"capability, got {msg}.",
|
||||||
)
|
)
|
||||||
@ -78,10 +293,6 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["UnrealIRCd"],
|
|
||||||
"UnrealIRCd sends a trailing space on CAP NAK: https://github.com/unrealircd/unrealircd/pull/148",
|
|
||||||
)
|
|
||||||
def testNakWhole(self):
|
def testNakWhole(self):
|
||||||
"""“The capability identifier set must be accepted as a whole, or
|
"""“The capability identifier set must be accepted as a whole, or
|
||||||
rejected entirely.”
|
rejected entirely.”
|
||||||
@ -89,7 +300,8 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
""" # noqa
|
""" # noqa
|
||||||
self.addClient(1)
|
self.addClient(1)
|
||||||
self.sendLine(1, "CAP LS 302")
|
self.sendLine(1, "CAP LS 302")
|
||||||
self.assertIn("multi-prefix", self.getCapLs(1))
|
if "multi-prefix" not in self.getCapLs(1):
|
||||||
|
raise CapabilityNotSupported("multi-prefix")
|
||||||
self.sendLine(1, "CAP REQ :foo multi-prefix bar")
|
self.sendLine(1, "CAP REQ :foo multi-prefix bar")
|
||||||
m = self.getRegistrationMessage(1)
|
m = self.getRegistrationMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
@ -123,16 +335,12 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
command="CAP",
|
command="CAP",
|
||||||
params=[ANYSTR, "ACK", "multi-prefix"],
|
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
|
||||||
fail_msg="Expected “CAP ACK :multi-prefix” after "
|
fail_msg="Expected “CAP ACK :multi-prefix” after "
|
||||||
"sending “CAP REQ :multi-prefix”, but got {msg}.",
|
"sending “CAP REQ :multi-prefix”, but got {msg}.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["UnrealIRCd"],
|
|
||||||
"UnrealIRCd sends a trailing space on CAP NAK: https://github.com/unrealircd/unrealircd/pull/148",
|
|
||||||
)
|
|
||||||
def testCapRemovalByClient(self):
|
def testCapRemovalByClient(self):
|
||||||
"""Test CAP LIST and removal of caps via CAP REQ :-tagname."""
|
"""Test CAP LIST and removal of caps via CAP REQ :-tagname."""
|
||||||
cap1 = "echo-message"
|
cap1 = "echo-message"
|
||||||
@ -140,8 +348,13 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
self.addClient(1)
|
self.addClient(1)
|
||||||
self.connectClient("sender")
|
self.connectClient("sender")
|
||||||
self.sendLine(1, "CAP LS 302")
|
self.sendLine(1, "CAP LS 302")
|
||||||
m = self.getRegistrationMessage(1)
|
caps = set()
|
||||||
if not ({cap1, cap2} <= set(m.params[2].split())):
|
while True:
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
caps.update(m.params[-1].split())
|
||||||
|
if m.params[2] != "*":
|
||||||
|
break
|
||||||
|
if not ({cap1, cap2} <= caps):
|
||||||
raise CapabilityNotSupported(f"{cap1} or {cap2}")
|
raise CapabilityNotSupported(f"{cap1} or {cap2}")
|
||||||
self.sendLine(1, f"CAP REQ :{cap1} {cap2}")
|
self.sendLine(1, f"CAP REQ :{cap1} {cap2}")
|
||||||
self.sendLine(1, "nick bar")
|
self.sendLine(1, "nick bar")
|
||||||
@ -167,17 +380,19 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertIn("time", m.tags, m)
|
self.assertIn("time", m.tags, m)
|
||||||
|
|
||||||
# remove the server-time cap
|
# remove the multi-prefix cap
|
||||||
self.sendLine(1, f"CAP REQ :-{cap2}")
|
self.sendLine(1, f"CAP REQ :-{cap2}")
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
# Must be either ACK or NAK
|
# Must be either ACK or NAK
|
||||||
if self.messageDiffers(m, command="CAP", params=[ANYSTR, "ACK", f"-{cap2}"]):
|
if self.messageDiffers(
|
||||||
|
m, command="CAP", params=[ANYSTR, "ACK", StrRe(f"-{cap2} ?")]
|
||||||
|
):
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m, command="CAP", params=[ANYSTR, "NAK", f"-{cap2}"]
|
m, command="CAP", params=[ANYSTR, "NAK", StrRe(f"-{cap2} ?")]
|
||||||
)
|
)
|
||||||
raise ImplementationChoice(f"Does not support CAP REQ -{cap2}")
|
raise ImplementationChoice(f"Does not support CAP REQ -{cap2}")
|
||||||
|
|
||||||
# server-time should be disabled
|
# multi-prefix should be disabled
|
||||||
self.sendLine(1, "CAP LIST")
|
self.sendLine(1, "CAP LIST")
|
||||||
messages = self.getMessages(1)
|
messages = self.getMessages(1)
|
||||||
cap_list = [m for m in messages if m.command == "CAP"][0]
|
cap_list = [m for m in messages if m.command == "CAP"][0]
|
||||||
@ -242,3 +457,31 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
fail_msg="Sending “CAP LIST” as first message got a reply "
|
fail_msg="Sending “CAP LIST” as first message got a reply "
|
||||||
"that is not “CAP * LIST :”: {msg}",
|
"that is not “CAP * LIST :”: {msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
def testNoMultiline301Response(self):
|
||||||
|
"""
|
||||||
|
Current version: "If the client supports CAP version 302, the server MAY send
|
||||||
|
multiple lines in response to CAP LS and CAP LIST." This should be read as
|
||||||
|
disallowing multiline responses to pre-302 clients.
|
||||||
|
-- <https://ircv3.net/specs/extensions/capability-negotiation#multiline-replies-to-cap-ls-and-cap-list>
|
||||||
|
""" # noqa
|
||||||
|
self.check301ResponsePreRegistration("bar", "CAP LS")
|
||||||
|
self.check301ResponsePreRegistration("qux", "CAP LS 301")
|
||||||
|
self.check301ResponsePostRegistration("baz", "CAP LS")
|
||||||
|
self.check301ResponsePostRegistration("bat", "CAP LS 301")
|
||||||
|
|
||||||
|
def check301ResponsePreRegistration(self, nick, cap_ls):
|
||||||
|
self.addClient(nick)
|
||||||
|
self.sendLine(nick, cap_ls)
|
||||||
|
self.sendLine(nick, "NICK " + nick)
|
||||||
|
self.sendLine(nick, "USER u s e r")
|
||||||
|
self.sendLine(nick, "CAP END")
|
||||||
|
responses = [msg for msg in self.skipToWelcome(nick) if msg.command == "CAP"]
|
||||||
|
self.assertLessEqual(len(responses), 1, responses)
|
||||||
|
|
||||||
|
def check301ResponsePostRegistration(self, nick, cap_ls):
|
||||||
|
self.connectClient(nick, name=nick)
|
||||||
|
self.sendLine(nick, cap_ls)
|
||||||
|
responses = [msg for msg in self.getMessages(nick) if msg.command == "CAP"]
|
||||||
|
self.assertLessEqual(len(responses), 1, responses)
|
||||||
|
@ -10,7 +10,7 @@ import pytest
|
|||||||
|
|
||||||
from irctest import cases, runner
|
from irctest import cases, runner
|
||||||
from irctest.irc_utils.junkdrawer import random_name
|
from irctest.irc_utils.junkdrawer import random_name
|
||||||
from irctest.patma import ANYSTR
|
from irctest.patma import ANYSTR, StrRe
|
||||||
|
|
||||||
CHATHISTORY_CAP = "draft/chathistory"
|
CHATHISTORY_CAP = "draft/chathistory"
|
||||||
EVENT_PLAYBACK_CAP = "draft/event-playback"
|
EVENT_PLAYBACK_CAP = "draft/event-playback"
|
||||||
@ -21,28 +21,6 @@ SUBCOMMANDS = ["LATEST", "BEFORE", "AFTER", "BETWEEN", "AROUND"]
|
|||||||
MYSQL_PASSWORD = ""
|
MYSQL_PASSWORD = ""
|
||||||
|
|
||||||
|
|
||||||
def validate_chathistory_batch(msgs):
|
|
||||||
batch_tag = None
|
|
||||||
closed_batch_tag = None
|
|
||||||
result = []
|
|
||||||
for msg in msgs:
|
|
||||||
if msg.command == "BATCH":
|
|
||||||
batch_param = msg.params[0]
|
|
||||||
if batch_tag is None and batch_param[0] == "+":
|
|
||||||
batch_tag = batch_param[1:]
|
|
||||||
elif batch_param[0] == "-":
|
|
||||||
closed_batch_tag = batch_param[1:]
|
|
||||||
elif (
|
|
||||||
msg.command == "PRIVMSG"
|
|
||||||
and batch_tag is not None
|
|
||||||
and msg.tags.get("batch") == batch_tag
|
|
||||||
):
|
|
||||||
if not msg.prefix.startswith("HistServ!"): # FIXME: ergo-specific
|
|
||||||
result.append(msg.to_history_message())
|
|
||||||
assert batch_tag == closed_batch_tag
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def skip_ngircd(f):
|
def skip_ngircd(f):
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
def newf(self, *args, **kwargs):
|
def newf(self, *args, **kwargs):
|
||||||
@ -56,10 +34,40 @@ def skip_ngircd(f):
|
|||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.mark_services
|
@cases.mark_services
|
||||||
class ChathistoryTestCase(cases.BaseServerTestCase):
|
class ChathistoryTestCase(cases.BaseServerTestCase):
|
||||||
|
def validate_chathistory_batch(self, msgs, target):
|
||||||
|
(start, *inner_msgs, end) = msgs
|
||||||
|
|
||||||
|
self.assertMessageMatch(
|
||||||
|
start, command="BATCH", params=[StrRe(r"\+.*"), "chathistory", target]
|
||||||
|
)
|
||||||
|
batch_tag = start.params[0][1:]
|
||||||
|
self.assertMessageMatch(end, command="BATCH", params=["-" + batch_tag])
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for msg in inner_msgs:
|
||||||
|
if (
|
||||||
|
msg.command in ("PRIVMSG", "TOPIC")
|
||||||
|
and batch_tag is not None
|
||||||
|
and msg.tags.get("batch") == batch_tag
|
||||||
|
):
|
||||||
|
if not msg.prefix.startswith("HistServ!"): # FIXME: ergo-specific
|
||||||
|
result.append(msg.to_history_message())
|
||||||
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
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")
|
||||||
@ -222,6 +230,47 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
|
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
|
||||||
self.validate_chathistory(subcommand, echo_messages, 1, chname)
|
self.validate_chathistory(subcommand, echo_messages, 1, chname)
|
||||||
|
|
||||||
|
@skip_ngircd
|
||||||
|
def testChathistoryNoEventPlayback(self):
|
||||||
|
"""Tests that non-messages don't appear in the chat history when event-playback
|
||||||
|
is not enabled."""
|
||||||
|
|
||||||
|
self.connectClient(
|
||||||
|
"bar",
|
||||||
|
capabilities=[
|
||||||
|
"message-tags",
|
||||||
|
"server-time",
|
||||||
|
"echo-message",
|
||||||
|
"batch",
|
||||||
|
"labeled-response",
|
||||||
|
"sasl",
|
||||||
|
CHATHISTORY_CAP,
|
||||||
|
],
|
||||||
|
skip_if_cap_nak=True,
|
||||||
|
)
|
||||||
|
chname = "#chan" + secrets.token_hex(12)
|
||||||
|
self.joinChannel(1, chname)
|
||||||
|
self.getMessages(1)
|
||||||
|
self.getMessages(1)
|
||||||
|
|
||||||
|
NUM_MESSAGES = 10
|
||||||
|
echo_messages = []
|
||||||
|
for i in range(NUM_MESSAGES):
|
||||||
|
self.sendLine(1, "TOPIC %s :this is topic %d" % (chname, i))
|
||||||
|
self.getMessages(1)
|
||||||
|
self.sendLine(1, "PRIVMSG %s :this is message %d" % (chname, i))
|
||||||
|
echo_messages.extend(
|
||||||
|
msg.to_history_message() for msg in self.getMessages(1)
|
||||||
|
)
|
||||||
|
time.sleep(0.002)
|
||||||
|
|
||||||
|
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
|
||||||
|
self.sendLine(1, "CHATHISTORY LATEST %s * 100" % chname)
|
||||||
|
(batch_open, *messages, batch_close) = self.getMessages(1)
|
||||||
|
self.assertMessageMatch(batch_open, command="BATCH")
|
||||||
|
self.assertMessageMatch(batch_close, command="BATCH")
|
||||||
|
self.assertEqual([msg for msg in messages if msg.command != "PRIVMSG"], [])
|
||||||
|
|
||||||
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
|
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
|
||||||
@skip_ngircd
|
@skip_ngircd
|
||||||
def testChathistoryEventPlayback(self, subcommand):
|
def testChathistoryEventPlayback(self, subcommand):
|
||||||
@ -246,21 +295,27 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
NUM_MESSAGES = 10
|
NUM_MESSAGES = 10
|
||||||
echo_messages = []
|
echo_messages = []
|
||||||
for i in range(NUM_MESSAGES):
|
for i in range(NUM_MESSAGES):
|
||||||
|
self.sendLine(1, "TOPIC %s :this is topic %d" % (chname, i))
|
||||||
|
echo_messages.extend(
|
||||||
|
msg.to_history_message() for msg in self.getMessages(1)
|
||||||
|
)
|
||||||
|
time.sleep(0.002)
|
||||||
|
|
||||||
self.sendLine(1, "PRIVMSG %s :this is message %d" % (chname, i))
|
self.sendLine(1, "PRIVMSG %s :this is message %d" % (chname, i))
|
||||||
echo_messages.extend(
|
echo_messages.extend(
|
||||||
msg.to_history_message() for msg in self.getMessages(1)
|
msg.to_history_message() for msg in self.getMessages(1)
|
||||||
)
|
)
|
||||||
time.sleep(0.002)
|
time.sleep(0.002)
|
||||||
|
|
||||||
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
|
self.validate_echo_messages(NUM_MESSAGES * 2, echo_messages)
|
||||||
self.validate_chathistory(subcommand, echo_messages, 1, chname)
|
self.validate_chathistory(subcommand, echo_messages, 1, chname)
|
||||||
|
|
||||||
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
|
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
|
||||||
@pytest.mark.private_chathistory
|
@pytest.mark.private_chathistory
|
||||||
@skip_ngircd
|
@skip_ngircd
|
||||||
def testChathistoryDMs(self, subcommand):
|
def testChathistoryDMs(self, subcommand):
|
||||||
c1 = "foo" + secrets.token_hex(12)
|
c1 = random_name("foo")
|
||||||
c2 = "bar" + secrets.token_hex(12)
|
c2 = random_name("bar")
|
||||||
self.controller.registerUser(self, c1, "sesame1")
|
self.controller.registerUser(self, c1, "sesame1")
|
||||||
self.controller.registerUser(self, c2, "sesame2")
|
self.controller.registerUser(self, c2, "sesame2")
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
@ -308,11 +363,14 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
time.sleep(0.002)
|
time.sleep(0.002)
|
||||||
|
|
||||||
|
self.getMessages(1)
|
||||||
|
self.getMessages(2)
|
||||||
|
|
||||||
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
|
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
|
||||||
self.validate_chathistory(subcommand, echo_messages, 1, c2)
|
self.validate_chathistory(subcommand, echo_messages, 1, c2)
|
||||||
self.validate_chathistory(subcommand, echo_messages, 2, c1)
|
self.validate_chathistory(subcommand, echo_messages, 2, c1)
|
||||||
|
|
||||||
c3 = "baz" + secrets.token_hex(12)
|
c3 = random_name("baz")
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
c3,
|
c3,
|
||||||
capabilities=[
|
capabilities=[
|
||||||
@ -401,189 +459,212 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
def _validate_chathistory_LATEST(self, echo_messages, user, chname):
|
def _validate_chathistory_LATEST(self, echo_messages, user, chname):
|
||||||
INCLUSIVE_LIMIT = len(echo_messages) * 2
|
INCLUSIVE_LIMIT = len(echo_messages) * 2
|
||||||
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, INCLUSIVE_LIMIT))
|
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, INCLUSIVE_LIMIT))
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages, result)
|
self.assertEqual(echo_messages, result)
|
||||||
|
|
||||||
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 5))
|
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 5))
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[-5:], result)
|
self.assertEqual(echo_messages[-5:], result)
|
||||||
|
|
||||||
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 1))
|
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 1))
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[-1:], result)
|
self.assertEqual(echo_messages[-1:], result)
|
||||||
|
|
||||||
self.sendLine(
|
if self._supports_msgid():
|
||||||
user,
|
self.sendLine(
|
||||||
"CHATHISTORY LATEST %s msgid=%s %d"
|
user,
|
||||||
% (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT),
|
"CHATHISTORY LATEST %s msgid=%s %d"
|
||||||
)
|
% (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT),
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
)
|
||||||
self.assertEqual(echo_messages[5:], result)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
self.assertEqual(echo_messages[5:], result)
|
||||||
|
|
||||||
self.sendLine(
|
if self._supports_timestamp():
|
||||||
user,
|
self.sendLine(
|
||||||
"CHATHISTORY LATEST %s timestamp=%s %d"
|
user,
|
||||||
% (chname, echo_messages[4].time, INCLUSIVE_LIMIT),
|
"CHATHISTORY LATEST %s timestamp=%s %d"
|
||||||
)
|
% (chname, echo_messages[4].time, INCLUSIVE_LIMIT),
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
)
|
||||||
self.assertEqual(echo_messages[5:], result)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
self.assertEqual(echo_messages[5:], result)
|
||||||
|
|
||||||
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
|
||||||
self.sendLine(
|
if self._supports_msgid():
|
||||||
user,
|
self.sendLine(
|
||||||
"CHATHISTORY BEFORE %s msgid=%s %d"
|
user,
|
||||||
% (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT),
|
"CHATHISTORY BEFORE %s msgid=%s %d"
|
||||||
)
|
% (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT),
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
)
|
||||||
self.assertEqual(echo_messages[:6], result)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
self.assertEqual(echo_messages[:6], result)
|
||||||
|
|
||||||
self.sendLine(
|
if self._supports_timestamp():
|
||||||
user,
|
self.sendLine(
|
||||||
"CHATHISTORY BEFORE %s timestamp=%s %d"
|
user,
|
||||||
% (chname, echo_messages[6].time, INCLUSIVE_LIMIT),
|
"CHATHISTORY BEFORE %s timestamp=%s %d"
|
||||||
)
|
% (chname, echo_messages[6].time, INCLUSIVE_LIMIT),
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
)
|
||||||
self.assertEqual(echo_messages[:6], result)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
self.assertEqual(echo_messages[:6], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY BEFORE %s timestamp=%s %d"
|
"CHATHISTORY BEFORE %s timestamp=%s %d"
|
||||||
% (chname, echo_messages[6].time, 2),
|
% (chname, echo_messages[6].time, 2),
|
||||||
)
|
)
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[4:6], result)
|
self.assertEqual(echo_messages[4:6], result)
|
||||||
|
|
||||||
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
|
||||||
self.sendLine(
|
if self._supports_msgid():
|
||||||
user,
|
self.sendLine(
|
||||||
"CHATHISTORY AFTER %s msgid=%s %d"
|
user,
|
||||||
% (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT),
|
"CHATHISTORY AFTER %s msgid=%s %d"
|
||||||
)
|
% (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT),
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
)
|
||||||
self.assertEqual(echo_messages[4:], result)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
self.assertEqual(echo_messages[4:], result)
|
||||||
|
|
||||||
self.sendLine(
|
if self._supports_timestamp():
|
||||||
user,
|
self.sendLine(
|
||||||
"CHATHISTORY AFTER %s timestamp=%s %d"
|
user,
|
||||||
% (chname, echo_messages[3].time, INCLUSIVE_LIMIT),
|
"CHATHISTORY AFTER %s timestamp=%s %d"
|
||||||
)
|
% (chname, echo_messages[3].time, INCLUSIVE_LIMIT),
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
)
|
||||||
self.assertEqual(echo_messages[4:], result)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
self.assertEqual(echo_messages[4:], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, 3),
|
"CHATHISTORY AFTER %s timestamp=%s %d"
|
||||||
)
|
% (chname, echo_messages[3].time, 3),
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
)
|
||||||
self.assertEqual(echo_messages[4:7], result)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
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
|
||||||
# BETWEEN forwards and backwards
|
if self._supports_msgid():
|
||||||
self.sendLine(
|
# BETWEEN forwards and backwards
|
||||||
user,
|
self.sendLine(
|
||||||
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
user,
|
||||||
% (
|
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
||||||
chname,
|
% (
|
||||||
echo_messages[0].msgid,
|
chname,
|
||||||
echo_messages[-1].msgid,
|
echo_messages[0].msgid,
|
||||||
INCLUSIVE_LIMIT,
|
echo_messages[-1].msgid,
|
||||||
),
|
INCLUSIVE_LIMIT,
|
||||||
)
|
),
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
)
|
||||||
self.assertEqual(echo_messages[1:-1], result)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
self.assertEqual(echo_messages[1:-1], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
||||||
% (
|
% (
|
||||||
chname,
|
chname,
|
||||||
echo_messages[-1].msgid,
|
echo_messages[-1].msgid,
|
||||||
echo_messages[0].msgid,
|
echo_messages[0].msgid,
|
||||||
INCLUSIVE_LIMIT,
|
INCLUSIVE_LIMIT,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[1:-1], result)
|
self.assertEqual(echo_messages[1:-1], result)
|
||||||
|
|
||||||
# BETWEEN forwards and backwards with a limit, should get
|
# BETWEEN forwards and backwards with a limit, should get
|
||||||
# different results this time
|
# different results this time
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
||||||
% (chname, echo_messages[0].msgid, echo_messages[-1].msgid, 3),
|
% (chname, echo_messages[0].msgid, echo_messages[-1].msgid, 3),
|
||||||
)
|
)
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[1:4], result)
|
self.assertEqual(echo_messages[1:4], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
||||||
% (chname, echo_messages[-1].msgid, echo_messages[0].msgid, 3),
|
% (chname, echo_messages[-1].msgid, echo_messages[0].msgid, 3),
|
||||||
)
|
)
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
self.assertEqual(echo_messages[-4:-1], result)
|
self.assertEqual(echo_messages[-4:-1], result)
|
||||||
|
|
||||||
# same stuff again but with timestamps
|
if self._supports_timestamp():
|
||||||
self.sendLine(
|
# same stuff again but with timestamps
|
||||||
user,
|
self.sendLine(
|
||||||
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
user,
|
||||||
% (chname, echo_messages[0].time, echo_messages[-1].time, INCLUSIVE_LIMIT),
|
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
||||||
)
|
% (
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
chname,
|
||||||
self.assertEqual(echo_messages[1:-1], result)
|
echo_messages[0].time,
|
||||||
self.sendLine(
|
echo_messages[-1].time,
|
||||||
user,
|
INCLUSIVE_LIMIT,
|
||||||
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
),
|
||||||
% (chname, echo_messages[-1].time, echo_messages[0].time, INCLUSIVE_LIMIT),
|
)
|
||||||
)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
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[0].time, echo_messages[-1].time, 3),
|
chname,
|
||||||
)
|
echo_messages[-1].time,
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
echo_messages[0].time,
|
||||||
self.assertEqual(echo_messages[1:4], result)
|
INCLUSIVE_LIMIT,
|
||||||
self.sendLine(
|
),
|
||||||
user,
|
)
|
||||||
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
% (chname, echo_messages[-1].time, echo_messages[0].time, 3),
|
self.assertEqual(echo_messages[1:-1], result)
|
||||||
)
|
self.sendLine(
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
user,
|
||||||
self.assertEqual(echo_messages[-4:-1], result)
|
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
||||||
|
% (chname, echo_messages[0].time, echo_messages[-1].time, 3),
|
||||||
|
)
|
||||||
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
self.assertEqual(echo_messages[1:4], result)
|
||||||
|
self.sendLine(
|
||||||
|
user,
|
||||||
|
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
||||||
|
% (chname, echo_messages[-1].time, echo_messages[0].time, 3),
|
||||||
|
)
|
||||||
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
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):
|
||||||
self.sendLine(
|
if self._supports_msgid():
|
||||||
user,
|
self.sendLine(
|
||||||
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 1),
|
user,
|
||||||
)
|
"CHATHISTORY AROUND %s msgid=%s %d"
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
% (chname, echo_messages[7].msgid, 1),
|
||||||
self.assertEqual([echo_messages[7]], result)
|
)
|
||||||
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
self.assertEqual([echo_messages[7]], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 3),
|
"CHATHISTORY AROUND %s msgid=%s %d"
|
||||||
)
|
% (chname, echo_messages[7].msgid, 3),
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
)
|
||||||
self.assertEqual(echo_messages[6:9], result)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
self.assertEqual(echo_messages[6:9], result)
|
||||||
|
|
||||||
self.sendLine(
|
if self._supports_timestamp():
|
||||||
user,
|
self.sendLine(
|
||||||
"CHATHISTORY AROUND %s timestamp=%s %d"
|
user,
|
||||||
% (chname, echo_messages[7].time, 3),
|
"CHATHISTORY AROUND %s timestamp=%s %d"
|
||||||
)
|
% (chname, echo_messages[7].time, 3),
|
||||||
result = validate_chathistory_batch(self.getMessages(user))
|
)
|
||||||
self.assertIn(echo_messages[7], result)
|
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
||||||
|
self.assertIn(echo_messages[7], result)
|
||||||
|
|
||||||
@pytest.mark.arbitrary_client_tags
|
@pytest.mark.arbitrary_client_tags
|
||||||
@skip_ngircd
|
@skip_ngircd
|
||||||
def testChathistoryTagmsg(self):
|
def testChathistoryTagmsg(self):
|
||||||
c1 = "foo" + secrets.token_hex(12)
|
c1 = random_name("foo")
|
||||||
c2 = "bar" + secrets.token_hex(12)
|
c2 = random_name("bar")
|
||||||
chname = "#chan" + secrets.token_hex(12)
|
chname = "#chan" + secrets.token_hex(12)
|
||||||
self.controller.registerUser(self, c1, "sesame1")
|
self.controller.registerUser(self, c1, "sesame1")
|
||||||
self.controller.registerUser(self, c2, "sesame2")
|
self.controller.registerUser(self, c2, "sesame2")
|
||||||
@ -682,8 +763,8 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
@skip_ngircd
|
@skip_ngircd
|
||||||
def testChathistoryDMClientOnlyTags(self):
|
def testChathistoryDMClientOnlyTags(self):
|
||||||
# regression test for Ergo #1411
|
# regression test for Ergo #1411
|
||||||
c1 = "foo" + secrets.token_hex(12)
|
c1 = random_name("foo")
|
||||||
c2 = "bar" + secrets.token_hex(12)
|
c2 = random_name("bar")
|
||||||
self.controller.registerUser(self, c1, "sesame1")
|
self.controller.registerUser(self, c1, "sesame1")
|
||||||
self.controller.registerUser(self, c2, "sesame2")
|
self.controller.registerUser(self, c2, "sesame2")
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
|
@ -44,8 +44,8 @@ class KeyTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"key",
|
"key",
|
||||||
["passphrase with spaces", "long" * 100, ""],
|
["passphrase with spaces", "long" * 100, "", " "],
|
||||||
ids=["spaces", "long", "empty"],
|
ids=["spaces", "long", "empty", "only-space"],
|
||||||
)
|
)
|
||||||
@cases.mark_specifications("RFC2812", "Modern")
|
@cases.mark_specifications("RFC2812", "Modern")
|
||||||
def testKeyValidation(self, key):
|
def testKeyValidation(self, key):
|
||||||
@ -84,6 +84,8 @@ class KeyTestCase(cases.BaseServerTestCase):
|
|||||||
"ngIRCd does not validate channel keys: "
|
"ngIRCd does not validate channel keys: "
|
||||||
"https://github.com/ngircd/ngircd/issues/290"
|
"https://github.com/ngircd/ngircd/issues/290"
|
||||||
)
|
)
|
||||||
|
if key == " " and self.controller.software_name == "irc2":
|
||||||
|
pytest.xfail("irc2 rewrites non-empty keys that contain only spaces")
|
||||||
|
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
self.joinChannel(1, "#chan")
|
self.joinChannel(1, "#chan")
|
||||||
|
67
irctest/server_tests/chmodes/modeis.py
Normal file
67
irctest/server_tests/chmodes/modeis.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
from irctest import cases
|
||||||
|
from irctest.numerics import RPL_CHANNELCREATED, RPL_CHANNELMODEIS
|
||||||
|
from irctest.patma import ANYSTR, ListRemainder, StrRe
|
||||||
|
|
||||||
|
|
||||||
|
class RplChannelModeIsTestCase(cases.BaseServerTestCase):
|
||||||
|
@cases.mark_specifications("Modern")
|
||||||
|
def testChannelModeIs(self):
|
||||||
|
"""Test RPL_CHANNELMODEIS and RPL_CHANNELCREATED as responses to
|
||||||
|
`MODE #channel`:
|
||||||
|
<https://modern.ircdocs.horse/#rplcreationtime-329>
|
||||||
|
<https://modern.ircdocs.horse/#rplchannelmodeis-324>
|
||||||
|
"""
|
||||||
|
expected_numerics = {RPL_CHANNELMODEIS, RPL_CHANNELCREATED}
|
||||||
|
if self.controller.software_name in ("irc2", "Sable"):
|
||||||
|
# irc2 and Sable don't use timestamps for conflict resolution,
|
||||||
|
# consequently they don't store the channel creation timestamp
|
||||||
|
# and don't send RPL_CHANNELCREATED
|
||||||
|
expected_numerics = {RPL_CHANNELMODEIS}
|
||||||
|
|
||||||
|
self.connectClient("chanop", name="chanop")
|
||||||
|
self.joinChannel("chanop", "#chan")
|
||||||
|
# i, n, and t are specified by RFC1459; some of them may be on by default,
|
||||||
|
# but after this, at least those three should be enabled:
|
||||||
|
self.sendLine("chanop", "MODE #chan +int")
|
||||||
|
self.getMessages("chanop")
|
||||||
|
|
||||||
|
self.sendLine("chanop", "MODE #chan")
|
||||||
|
messages = self.getMessages("chanop")
|
||||||
|
self.assertEqual(expected_numerics, {msg.command for msg in messages})
|
||||||
|
for message in messages:
|
||||||
|
if message.command == RPL_CHANNELMODEIS:
|
||||||
|
# the final parameters are the mode string (e.g. `+int`),
|
||||||
|
# and then optionally any mode parameters (in case the ircd
|
||||||
|
# lists a mode that takes a parameter)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
message,
|
||||||
|
command=RPL_CHANNELMODEIS,
|
||||||
|
params=["chanop", "#chan", ListRemainder(ANYSTR, min_length=1)],
|
||||||
|
)
|
||||||
|
final_param = message.params[2]
|
||||||
|
self.assertEqual(final_param[0], "+")
|
||||||
|
enabled_modes = list(final_param[1:])
|
||||||
|
break
|
||||||
|
|
||||||
|
self.assertLessEqual({"i", "n", "t"}, set(enabled_modes))
|
||||||
|
|
||||||
|
# remove all the modes listed by RPL_CHANNELMODEIS
|
||||||
|
self.sendLine("chanop", f"MODE #chan -{''.join(enabled_modes)}")
|
||||||
|
response = self.getMessage("chanop")
|
||||||
|
# we should get something like: MODE #chan -int
|
||||||
|
self.assertMessageMatch(
|
||||||
|
response, command="MODE", params=["#chan", StrRe("^-.*")]
|
||||||
|
)
|
||||||
|
self.assertEqual(set(response.params[1][1:]), set(enabled_modes))
|
||||||
|
|
||||||
|
self.sendLine("chanop", "MODE #chan")
|
||||||
|
messages = self.getMessages("chanop")
|
||||||
|
self.assertEqual(expected_numerics, {msg.command for msg in messages})
|
||||||
|
# all modes have been disabled; the correct representation of this is `+`
|
||||||
|
for message in messages:
|
||||||
|
if message.command == RPL_CHANNELMODEIS:
|
||||||
|
self.assertMessageMatch(
|
||||||
|
message,
|
||||||
|
command=RPL_CHANNELMODEIS,
|
||||||
|
params=["chanop", "#chan", "+"],
|
||||||
|
)
|
31
irctest/server_tests/chmodes/no_ctcp.py
Normal file
31
irctest/server_tests/chmodes/no_ctcp.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
from irctest import cases
|
||||||
|
from irctest.numerics import ERR_CANNOTSENDTOCHAN
|
||||||
|
|
||||||
|
|
||||||
|
class NoCTCPChannelModeTestCase(cases.BaseServerTestCase):
|
||||||
|
@cases.mark_specifications("Ergo")
|
||||||
|
def testNoCTCPChannelMode(self):
|
||||||
|
"""Test Ergo's +C channel mode that blocks CTCPs."""
|
||||||
|
self.connectClient("bar")
|
||||||
|
self.joinChannel(1, "#chan")
|
||||||
|
self.sendLine(1, "MODE #chan +C")
|
||||||
|
self.getMessages(1)
|
||||||
|
|
||||||
|
self.connectClient("qux")
|
||||||
|
self.joinChannel(2, "#chan")
|
||||||
|
self.getMessages(2)
|
||||||
|
|
||||||
|
self.sendLine(1, "PRIVMSG #chan :\x01ACTION hi\x01")
|
||||||
|
self.getMessages(1)
|
||||||
|
ms = self.getMessages(2)
|
||||||
|
self.assertEqual(len(ms), 1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
ms[0], command="PRIVMSG", params=["#chan", "\x01ACTION hi\x01"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "PRIVMSG #chan :\x01PING 1473523796 918320\x01")
|
||||||
|
ms = self.getMessages(1)
|
||||||
|
self.assertEqual(len(ms), 1)
|
||||||
|
self.assertMessageMatch(ms[0], command=ERR_CANNOTSENDTOCHAN)
|
||||||
|
ms = self.getMessages(2)
|
||||||
|
self.assertEqual(ms, [])
|
38
irctest/server_tests/chmodes/no_external.py
Normal file
38
irctest/server_tests/chmodes/no_external.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""
|
||||||
|
Channel "no external messages" mode (`RFC 1459
|
||||||
|
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.3.1>`__,
|
||||||
|
`Modern <https://modern.ircdocs.horse/#no-external-messages-mode>`__)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from irctest import cases
|
||||||
|
from irctest.numerics import ERR_CANNOTSENDTOCHAN
|
||||||
|
|
||||||
|
|
||||||
|
class NoExternalMessagesTestCase(cases.BaseServerTestCase):
|
||||||
|
@cases.mark_specifications("RFC1459", "Modern")
|
||||||
|
def testNoExternalMessagesMode(self):
|
||||||
|
# test the +n channel mode
|
||||||
|
self.connectClient("chanop", name="chanop")
|
||||||
|
self.joinChannel("chanop", "#chan")
|
||||||
|
self.sendLine("chanop", "MODE #chan +n")
|
||||||
|
self.getMessages("chanop")
|
||||||
|
|
||||||
|
self.connectClient("baz", name="baz")
|
||||||
|
# this message should be suppressed completely by +n
|
||||||
|
self.sendLine("baz", "PRIVMSG #chan :hi from baz")
|
||||||
|
replies = self.getMessages("baz")
|
||||||
|
reply_cmds = {reply.command for reply in replies}
|
||||||
|
self.assertIn(ERR_CANNOTSENDTOCHAN, reply_cmds)
|
||||||
|
self.assertEqual(self.getMessages("chanop"), [])
|
||||||
|
|
||||||
|
# set the channel to -n: baz should be able to send now
|
||||||
|
self.sendLine("chanop", "MODE #chan -n")
|
||||||
|
replies = self.getMessages("chanop")
|
||||||
|
modeLines = [line for line in replies if line.command == "MODE"]
|
||||||
|
self.assertMessageMatch(modeLines[0], command="MODE", params=["#chan", "-n"])
|
||||||
|
self.sendLine("baz", "PRIVMSG #chan :hi again from baz")
|
||||||
|
self.getMessages("baz")
|
||||||
|
relays = self.getMessages("chanop")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
relays[0], command="PRIVMSG", params=["#chan", "hi again from baz"]
|
||||||
|
)
|
159
irctest/server_tests/chmodes/operator.py
Normal file
159
irctest/server_tests/chmodes/operator.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
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["accounts"].update(
|
ergo_config=lambda config: config["server"].update(
|
||||||
{"nick-reservation": {"enabled": True, "method": "strict"}}
|
{"casemapping": "precis"},
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -5,10 +5,12 @@ Tests section 4.1 of RFC 1459.
|
|||||||
TODO: cross-reference Modern and RFC 2812 too
|
TODO: cross-reference Modern and RFC 2812 too
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
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 ANYSTR, StrRe
|
from irctest.patma import ANYLIST, ANYSTR, OptStrRe, StrRe
|
||||||
|
|
||||||
|
|
||||||
class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
||||||
@ -83,6 +85,92 @@ 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
|
||||||
@ -133,7 +221,7 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertNotEqual(
|
self.assertNotEqual(
|
||||||
m.command,
|
m.command,
|
||||||
"001",
|
"001",
|
||||||
"Received 001 after registering with the nick of a " "registered user.",
|
"Received 001 after registering with the nick of a registered user.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def testEarlyNickCollision(self):
|
def testEarlyNickCollision(self):
|
||||||
@ -206,3 +294,58 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
|||||||
command=ERR_NEEDMOREPARAMS,
|
command=ERR_NEEDMOREPARAMS,
|
||||||
params=[StrRe(r"(\*|foo)"), "USER", ANYSTR],
|
params=[StrRe(r"(\*|foo)"), "USER", ANYSTR],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def testNonutf8Realname(self):
|
||||||
|
self.addClient()
|
||||||
|
self.sendLine(1, "NICK foo")
|
||||||
|
line = b"USER username * * :i\xe8rc\xe9\r\n"
|
||||||
|
print("1 -> S (repr): " + repr(line))
|
||||||
|
self.clients[1].conn.sendall(line)
|
||||||
|
for _ in range(10):
|
||||||
|
time.sleep(1)
|
||||||
|
d = self.clients[1].conn.recv(10000)
|
||||||
|
self.assertTrue(d, "Server closed connection")
|
||||||
|
print("S -> 1 (repr): " + repr(d))
|
||||||
|
if b" 001 " in d:
|
||||||
|
break
|
||||||
|
if b"ERROR " in d or b" FAIL " in d:
|
||||||
|
# Rejected; nothing more to test.
|
||||||
|
return
|
||||||
|
for line in d.split(b"\r\n"):
|
||||||
|
if line.startswith(b"PING "):
|
||||||
|
line = line.replace(b"PING", b"PONG") + b"\r\n"
|
||||||
|
print("1 -> S (repr): " + repr(line))
|
||||||
|
self.clients[1].conn.sendall(line)
|
||||||
|
else:
|
||||||
|
self.assertTrue(False, "stuck waiting")
|
||||||
|
self.sendLine(1, "WHOIS foo")
|
||||||
|
time.sleep(3) # for ngIRCd
|
||||||
|
d = self.clients[1].conn.recv(10000)
|
||||||
|
print("S -> 1 (repr): " + repr(d))
|
||||||
|
self.assertIn(b"username", d)
|
||||||
|
|
||||||
|
def testNonutf8Username(self):
|
||||||
|
self.addClient()
|
||||||
|
self.sendLine(1, "NICK foo")
|
||||||
|
self.sendLine(1, "USER 😊😊😊😊😊😊😊😊😊😊 * * :realname")
|
||||||
|
for _ in range(10):
|
||||||
|
time.sleep(1)
|
||||||
|
d = self.clients[1].conn.recv(10000)
|
||||||
|
self.assertTrue(d, "Server closed connection")
|
||||||
|
print("S -> 1 (repr): " + repr(d))
|
||||||
|
if b" 001 " in d:
|
||||||
|
break
|
||||||
|
if b" 468" in d or b"ERROR " in d:
|
||||||
|
# Rejected; nothing more to test.
|
||||||
|
return
|
||||||
|
for line in d.split(b"\r\n"):
|
||||||
|
if line.startswith(b"PING "):
|
||||||
|
line = line.replace(b"PING", b"PONG") + b"\r\n"
|
||||||
|
print("1 -> S (repr): " + repr(line))
|
||||||
|
self.clients[1].conn.sendall(line)
|
||||||
|
else:
|
||||||
|
self.assertTrue(False, "stuck waiting")
|
||||||
|
self.sendLine(1, "WHOIS foo")
|
||||||
|
d = self.clients[1].conn.recv(10000)
|
||||||
|
print("S -> 1 (repr): " + repr(d))
|
||||||
|
self.assertIn(b"realname", d)
|
||||||
|
@ -22,23 +22,20 @@ class EchoMessageTestCase(cases.BaseServerTestCase):
|
|||||||
@cases.mark_capabilities("echo-message")
|
@cases.mark_capabilities("echo-message")
|
||||||
def testEchoMessage(self, command, solo, server_time):
|
def testEchoMessage(self, command, solo, server_time):
|
||||||
"""<http://ircv3.net/specs/extensions/echo-message-3.2.html>"""
|
"""<http://ircv3.net/specs/extensions/echo-message-3.2.html>"""
|
||||||
if server_time:
|
capabilities = ["server-time"] if server_time else []
|
||||||
self.connectClient(
|
|
||||||
"baz",
|
self.connectClient(
|
||||||
capabilities=["echo-message", "server-time"],
|
"baz",
|
||||||
skip_if_cap_nak=True,
|
capabilities=["echo-message", *capabilities],
|
||||||
)
|
skip_if_cap_nak=True,
|
||||||
else:
|
)
|
||||||
self.connectClient(
|
|
||||||
"baz",
|
|
||||||
capabilities=["echo-message", "server-time"],
|
|
||||||
skip_if_cap_nak=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "JOIN #chan")
|
self.sendLine(1, "JOIN #chan")
|
||||||
|
|
||||||
|
# Synchronize
|
||||||
|
self.getMessages(1)
|
||||||
|
|
||||||
if not solo:
|
if not solo:
|
||||||
capabilities = ["server-time"] if server_time else None
|
|
||||||
self.connectClient("qux", capabilities=capabilities)
|
self.connectClient("qux", capabilities=capabilities)
|
||||||
self.sendLine(2, "JOIN #chan")
|
self.sendLine(2, "JOIN #chan")
|
||||||
|
|
||||||
|
@ -360,8 +360,8 @@ class InviteTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
|
|
||||||
self.sendLine(1, "JOIN #chan")
|
self.sendLine(1, "JOIN #chan")
|
||||||
self.sendLine(2, "JOIN #chan")
|
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
|
self.sendLine(2, "JOIN #chan")
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
|
|
||||||
|
@ -9,6 +9,38 @@ 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):
|
||||||
@ -24,7 +56,8 @@ class IsupportTestCase(cases.BaseServerTestCase):
|
|||||||
return
|
return
|
||||||
|
|
||||||
m = re.match(
|
m = re.match(
|
||||||
r"\((?P<modes>[a-zA-Z]+)\)(?P<prefixes>\S+)", self.server_support["PREFIX"]
|
r"^\((?P<modes>[a-zA-Z]+)\)(?P<prefixes>\S+)$",
|
||||||
|
self.server_support["PREFIX"],
|
||||||
)
|
)
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
m,
|
m,
|
||||||
@ -85,5 +118,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,7 +6,6 @@ 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,
|
||||||
@ -61,6 +60,7 @@ 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,33 +75,23 @@ 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.assertIn(
|
self.assertMessageMatch(
|
||||||
len(m.params),
|
m, params=["foo", StrRe(r"[=\*@]"), "#chan", StrRe("[@+]?foo")]
|
||||||
(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(
|
self.connectClient("bar")
|
||||||
params[1],
|
self.sendLine(2, "JOIN #chan")
|
||||||
"=*@",
|
|
||||||
|
for m in self.getMessages(2):
|
||||||
|
if m.command == "353":
|
||||||
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
fail_msg="Bad channel prefix: {item} not in {list}: {msg}",
|
params=[
|
||||||
)
|
"bar",
|
||||||
self.assertEqual(
|
StrRe(r"[=\*@]"),
|
||||||
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):
|
||||||
@ -115,34 +105,8 @@ 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.assertIn(
|
self.assertMessageMatch(
|
||||||
len(m.params),
|
m, params=["foo", StrRe(r"[=\*@]"), "#chan", StrRe("[@+]?foo")]
|
||||||
(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):
|
||||||
@ -236,3 +200,78 @@ 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"],
|
||||||
|
)
|
||||||
|
67
irctest/server_tests/kill.py
Normal file
67
irctest/server_tests/kill.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
"""
|
||||||
|
The KILL command (`Modern <https://modern.ircdocs.horse/#kill-message>`__)
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from irctest import cases
|
||||||
|
from irctest.numerics import ERR_NOPRIVILEGES, RPL_YOUREOPER
|
||||||
|
|
||||||
|
|
||||||
|
class KillTestCase(cases.BaseServerTestCase):
|
||||||
|
@cases.mark_specifications("Modern")
|
||||||
|
@cases.xfailIfSoftware(["Sable"], "https://github.com/Libera-Chat/sable/issues/154")
|
||||||
|
def testKill(self):
|
||||||
|
self.connectClient("ircop", name="ircop")
|
||||||
|
self.connectClient("alice", name="alice")
|
||||||
|
self.connectClient("bob", name="bob")
|
||||||
|
|
||||||
|
self.sendLine("ircop", "OPER operuser operpassword")
|
||||||
|
self.assertIn(
|
||||||
|
RPL_YOUREOPER,
|
||||||
|
[m.command for m in self.getMessages("ircop")],
|
||||||
|
fail_msg="OPER failed",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine("alice", "KILL bob :some arbitrary reason")
|
||||||
|
self.assertIn(
|
||||||
|
ERR_NOPRIVILEGES,
|
||||||
|
[m.command for m in self.getMessages("alice")],
|
||||||
|
fail_msg="unprivileged KILL not rejected",
|
||||||
|
)
|
||||||
|
# bob is not killed
|
||||||
|
self.getMessages("bob")
|
||||||
|
|
||||||
|
self.sendLine("alice", "KILL alice :some arbitrary reason")
|
||||||
|
self.assertIn(
|
||||||
|
ERR_NOPRIVILEGES,
|
||||||
|
[m.command for m in self.getMessages("alice")],
|
||||||
|
fail_msg="unprivileged KILL not rejected",
|
||||||
|
)
|
||||||
|
# alice is not killed
|
||||||
|
self.getMessages("alice")
|
||||||
|
|
||||||
|
# privileged KILL should succeed
|
||||||
|
self.sendLine("ircop", "KILL alice :some arbitrary reason")
|
||||||
|
self.getMessages("ircop")
|
||||||
|
self.assertDisconnected("alice")
|
||||||
|
|
||||||
|
self.sendLine("ircop", "KILL bob :some arbitrary reason")
|
||||||
|
self.getMessages("ircop")
|
||||||
|
self.assertDisconnected("bob")
|
||||||
|
|
||||||
|
@cases.mark_specifications("Ergo")
|
||||||
|
def testKillOneArgument(self):
|
||||||
|
self.connectClient("ircop", name="ircop")
|
||||||
|
self.connectClient("alice", name="alice")
|
||||||
|
|
||||||
|
self.sendLine("ircop", "OPER operuser operpassword")
|
||||||
|
self.assertIn(
|
||||||
|
RPL_YOUREOPER,
|
||||||
|
[m.command for m in self.getMessages("ircop")],
|
||||||
|
fail_msg="OPER failed",
|
||||||
|
)
|
||||||
|
|
||||||
|
# 1-argument kill command, accepted by Ergo and some implementations
|
||||||
|
self.sendLine("ircop", "KILL alice")
|
||||||
|
self.getMessages("ircop")
|
||||||
|
self.assertDisconnected("alice")
|
@ -12,6 +12,7 @@ import pytest
|
|||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import ERR_UNKNOWNCOMMAND
|
from irctest.numerics import ERR_UNKNOWNCOMMAND
|
||||||
from irctest.patma import ANYDICT, ANYOPTSTR, NotStrRe, RemainingKeys, StrRe
|
from irctest.patma import ANYDICT, ANYOPTSTR, NotStrRe, RemainingKeys, StrRe
|
||||||
|
from irctest.runner import OptionalExtensionNotSupported
|
||||||
|
|
||||||
|
|
||||||
class LabeledResponsesTestCase(cases.BaseServerTestCase):
|
class LabeledResponsesTestCase(cases.BaseServerTestCase):
|
||||||
@ -22,7 +23,10 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase):
|
|||||||
capabilities=["echo-message", "batch", "labeled-response"],
|
capabilities=["echo-message", "batch", "labeled-response"],
|
||||||
skip_if_cap_nak=True,
|
skip_if_cap_nak=True,
|
||||||
)
|
)
|
||||||
|
if int(self.targmax.get("PRIVMSG", "1") or "4") < 3:
|
||||||
|
raise OptionalExtensionNotSupported("PRIVMSG to multiple targets")
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
|
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
"bar",
|
"bar",
|
||||||
capabilities=["echo-message", "batch", "labeled-response"],
|
capabilities=["echo-message", "batch", "labeled-response"],
|
||||||
|
@ -3,6 +3,13 @@ from irctest.numerics import ERR_UNKNOWNCOMMAND, RPL_ENDOFLINKS, RPL_LINKS
|
|||||||
from irctest.patma import ANYSTR, StrRe
|
from irctest.patma import ANYSTR, StrRe
|
||||||
|
|
||||||
|
|
||||||
|
def _server_info_regexp(case: cases.BaseServerTestCase) -> str:
|
||||||
|
if case.controller.software_name == "Sable":
|
||||||
|
return ".+"
|
||||||
|
else:
|
||||||
|
return "test server"
|
||||||
|
|
||||||
|
|
||||||
class LinksTestCase(cases.BaseServerTestCase):
|
class LinksTestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
||||||
def testLinksSingleServer(self):
|
def testLinksSingleServer(self):
|
||||||
@ -56,7 +63,7 @@ class LinksTestCase(cases.BaseServerTestCase):
|
|||||||
"nick",
|
"nick",
|
||||||
"My.Little.Server",
|
"My.Little.Server",
|
||||||
"My.Little.Server",
|
"My.Little.Server",
|
||||||
StrRe("0 (0042 )?test server"),
|
StrRe(f"0 (0042 )?{_server_info_regexp(self)}"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -110,7 +117,7 @@ class ServicesLinksTestCase(cases.BaseServerTestCase):
|
|||||||
# This server redacts links
|
# This server redacts links
|
||||||
return
|
return
|
||||||
|
|
||||||
messages.sort(key=lambda m: m.params[-1])
|
messages.sort(key=lambda m: tuple(m.params))
|
||||||
|
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
messages.pop(0),
|
messages.pop(0),
|
||||||
@ -119,7 +126,7 @@ class ServicesLinksTestCase(cases.BaseServerTestCase):
|
|||||||
"nick",
|
"nick",
|
||||||
"My.Little.Server",
|
"My.Little.Server",
|
||||||
"My.Little.Server",
|
"My.Little.Server",
|
||||||
StrRe("0 (0042 )?test server"),
|
StrRe(f"0 (0042 )?{_server_info_regexp(self)}"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
@ -127,9 +134,9 @@ class ServicesLinksTestCase(cases.BaseServerTestCase):
|
|||||||
command=RPL_LINKS,
|
command=RPL_LINKS,
|
||||||
params=[
|
params=[
|
||||||
"nick",
|
"nick",
|
||||||
"services.example.org",
|
"My.Little.Services",
|
||||||
"My.Little.Server",
|
"My.Little.Server",
|
||||||
StrRe("1 .+"), # SID instead of description for Anope...
|
StrRe("[01] .+"), # SID instead of description for Anope...
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ The PRIVMSG and NOTICE commands.
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
class PrivmsgTestCase(cases.BaseServerTestCase):
|
class PrivmsgTestCase(cases.BaseServerTestCase):
|
||||||
@ -12,6 +13,7 @@ class PrivmsgTestCase(cases.BaseServerTestCase):
|
|||||||
"""<https://tools.ietf.org/html/rfc2812#section-3.3.1>"""
|
"""<https://tools.ietf.org/html/rfc2812#section-3.3.1>"""
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.sendLine(1, "JOIN #chan")
|
self.sendLine(1, "JOIN #chan")
|
||||||
|
self.getMessages(1) # synchronize
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
self.sendLine(2, "JOIN #chan")
|
self.sendLine(2, "JOIN #chan")
|
||||||
self.getMessages(2) # synchronize
|
self.getMessages(2) # synchronize
|
||||||
@ -32,6 +34,48 @@ class PrivmsgTestCase(cases.BaseServerTestCase):
|
|||||||
# ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL, or ERR_CANNOTSENDTOCHAN
|
# ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL, or ERR_CANNOTSENDTOCHAN
|
||||||
self.assertIn(msg.command, ("401", "403", "404"))
|
self.assertIn(msg.command, ("401", "403", "404"))
|
||||||
|
|
||||||
|
@cases.mark_specifications("RFC1459", "RFC2812")
|
||||||
|
def testPrivmsgToUser(self):
|
||||||
|
"""<https://tools.ietf.org/html/rfc2812#section-3.3.1>"""
|
||||||
|
self.connectClient("foo")
|
||||||
|
self.connectClient("bar")
|
||||||
|
self.sendLine(1, "PRIVMSG bar :hey there!")
|
||||||
|
self.getMessages(1)
|
||||||
|
pms = [msg for msg in self.getMessages(2) if msg.command == "PRIVMSG"]
|
||||||
|
self.assertEqual(len(pms), 1)
|
||||||
|
self.assertMessageMatch(pms[0], command="PRIVMSG", params=["bar", "hey there!"])
|
||||||
|
|
||||||
|
@cases.mark_specifications("RFC1459", "RFC2812")
|
||||||
|
def testPrivmsgNonexistentUser(self):
|
||||||
|
"""<https://tools.ietf.org/html/rfc2812#section-3.3.1>"""
|
||||||
|
self.connectClient("foo")
|
||||||
|
self.sendLine(1, "PRIVMSG bar :hey there!")
|
||||||
|
msg = self.getMessage(1)
|
||||||
|
# ERR_NOSUCHNICK: 401 <sender> <recipient> :No such nick
|
||||||
|
self.assertMessageMatch(msg, command="401", params=["foo", "bar", ANYSTR])
|
||||||
|
|
||||||
|
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
||||||
|
@cases.xfailIfSoftware(
|
||||||
|
["irc2"],
|
||||||
|
"replies with ERR_NEEDMOREPARAMS instead of ERR_NOTEXTTOSEND",
|
||||||
|
)
|
||||||
|
def testEmptyPrivmsg(self):
|
||||||
|
self.connectClient("foo")
|
||||||
|
self.sendLine(1, "JOIN #chan")
|
||||||
|
self.getMessages(1) # synchronize
|
||||||
|
self.connectClient("bar")
|
||||||
|
self.sendLine(2, "JOIN #chan")
|
||||||
|
self.getMessages(2) # synchronize
|
||||||
|
self.getMessages(1) # synchronize
|
||||||
|
self.sendLine(1, "PRIVMSG #chan :")
|
||||||
|
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(1),
|
||||||
|
command="412", # ERR_NOTEXTTOSEND
|
||||||
|
params=["foo", ANYSTR],
|
||||||
|
)
|
||||||
|
self.assertEqual(self.getMessages(2), [])
|
||||||
|
|
||||||
|
|
||||||
class NoticeTestCase(cases.BaseServerTestCase):
|
class NoticeTestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812")
|
@cases.mark_specifications("RFC1459", "RFC2812")
|
||||||
@ -80,8 +124,13 @@ class NoticeTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
class TagsTestCase(cases.BaseServerTestCase):
|
class TagsTestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_capabilities("message-tags")
|
@cases.mark_capabilities("message-tags")
|
||||||
@cases.xfailIfSoftware(
|
@cases.xfailIf(
|
||||||
["UnrealIRCd"], "https://bugs.unrealircd.org/view.php?id=5947"
|
lambda self: bool(
|
||||||
|
self.controller.software_name == "UnrealIRCd"
|
||||||
|
and self.controller.software_version == 5
|
||||||
|
),
|
||||||
|
"UnrealIRCd <6.0.7 dropped messages with excessively large tags: "
|
||||||
|
"https://bugs.unrealircd.org/view.php?id=5947",
|
||||||
)
|
)
|
||||||
def testLineTooLong(self):
|
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)
|
||||||
@ -91,9 +140,9 @@ class TagsTestCase(cases.BaseServerTestCase):
|
|||||||
self.joinChannel(1, "#xyz")
|
self.joinChannel(1, "#xyz")
|
||||||
monsterMessage = "@+clientOnlyTagExample=" + "a" * 4096 + " PRIVMSG #xyz hi!"
|
monsterMessage = "@+clientOnlyTagExample=" + "a" * 4096 + " PRIVMSG #xyz hi!"
|
||||||
self.sendLine(1, monsterMessage)
|
self.sendLine(1, monsterMessage)
|
||||||
self.assertEqual(self.getMessages(2), [], "overflowing message was relayed")
|
|
||||||
replies = self.getMessages(1)
|
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))
|
||||||
|
self.assertEqual(self.getMessages(2), [], "overflowing message was relayed")
|
||||||
|
|
||||||
|
|
||||||
class LengthLimitTestCase(cases.BaseServerTestCase):
|
class LengthLimitTestCase(cases.BaseServerTestCase):
|
||||||
|
@ -6,8 +6,8 @@ from irctest import cases
|
|||||||
|
|
||||||
|
|
||||||
class MetadataTestCase(cases.BaseServerTestCase):
|
class MetadataTestCase(cases.BaseServerTestCase):
|
||||||
valid_metadata_keys = {"valid_key1", "valid_key2"}
|
valid_metadata_keys = {"display-name", "avatar"}
|
||||||
invalid_metadata_keys = {"invalid_key1", "invalid_key2"}
|
invalid_metadata_keys = {"indisplay-name", "inavatar"}
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3", deprecated=True)
|
@cases.mark_specifications("IRCv3", deprecated=True)
|
||||||
def testInIsupport(self):
|
def testInIsupport(self):
|
||||||
@ -36,7 +36,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
def testGetOneUnsetValid(self):
|
def testGetOneUnsetValid(self):
|
||||||
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
|
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.sendLine(1, "METADATA * GET valid_key1")
|
self.sendLine(1, "METADATA * GET display-name")
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
@ -52,7 +52,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
|
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
|
||||||
"""
|
"""
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.sendLine(1, "METADATA * GET valid_key1 valid_key2")
|
self.sendLine(1, "METADATA * GET display-name avatar")
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
@ -62,10 +62,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
m.params[1],
|
m.params[1],
|
||||||
"valid_key1",
|
"display-name",
|
||||||
m,
|
m,
|
||||||
fail_msg="Response to “METADATA * GET valid_key1 valid_key2” "
|
fail_msg="Response to “METADATA * GET display-name avatar” "
|
||||||
"did not respond to valid_key1 first: {msg}",
|
"did not respond to display-name first: {msg}",
|
||||||
)
|
)
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
@ -76,10 +76,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
m.params[1],
|
m.params[1],
|
||||||
"valid_key2",
|
"avatar",
|
||||||
m,
|
m,
|
||||||
fail_msg="Response to “METADATA * GET valid_key1 valid_key2” "
|
fail_msg="Response to “METADATA * GET display-name avatar” "
|
||||||
"did not respond to valid_key2 as second response: {msg}",
|
"did not respond to avatar as second response: {msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3", deprecated=True)
|
@cases.mark_specifications("IRCv3", deprecated=True)
|
||||||
@ -135,7 +135,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
m.params[1],
|
m.params[1],
|
||||||
"valid_key1",
|
"display-name",
|
||||||
m,
|
m,
|
||||||
fail_msg="Second param of 761 after setting “{expects}” to "
|
fail_msg="Second param of 761 after setting “{expects}” to "
|
||||||
"“{}” is not “{expects}”: {msg}.",
|
"“{}” is not “{expects}”: {msg}.",
|
||||||
@ -190,7 +190,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
def testSetGetValid(self):
|
def testSetGetValid(self):
|
||||||
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
|
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.assertSetGetValue("*", "valid_key1", "myvalue")
|
self.assertSetGetValue("*", "display-name", "myvalue")
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3", deprecated=True)
|
@cases.mark_specifications("IRCv3", deprecated=True)
|
||||||
def testSetGetZeroCharInValue(self):
|
def testSetGetZeroCharInValue(self):
|
||||||
@ -198,7 +198,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
|
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
|
||||||
"""
|
"""
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.assertSetGetValue("*", "valid_key1", "zero->\0<-zero", "zero->\\0<-zero")
|
self.assertSetGetValue("*", "display-name", "zero->\0<-zero", "zero->\\0<-zero")
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3", deprecated=True)
|
@cases.mark_specifications("IRCv3", deprecated=True)
|
||||||
def testSetGetHeartInValue(self):
|
def testSetGetHeartInValue(self):
|
||||||
@ -209,7 +209,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.assertSetGetValue(
|
self.assertSetGetValue(
|
||||||
"*",
|
"*",
|
||||||
"valid_key1",
|
"display-name",
|
||||||
"->{}<-".format(heart),
|
"->{}<-".format(heart),
|
||||||
"zero->{}<-zero".format(heart.encode()),
|
"zero->{}<-zero".format(heart.encode()),
|
||||||
)
|
)
|
||||||
@ -223,7 +223,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
# Sending directly because it is not valid UTF-8 so Python would
|
# Sending directly because it is not valid UTF-8 so Python would
|
||||||
# not like it
|
# not like it
|
||||||
self.clients[1].conn.sendall(
|
self.clients[1].conn.sendall(
|
||||||
b"METADATA * SET valid_key1 " b":invalid UTF-8 ->\xc3<-\r\n"
|
b"METADATA * SET display-name " b":invalid UTF-8 ->\xc3<-\r\n"
|
||||||
)
|
)
|
||||||
commands = {m.command for m in self.getMessages(1)}
|
commands = {m.command for m in self.getMessages(1)}
|
||||||
self.assertNotIn(
|
self.assertNotIn(
|
||||||
@ -233,7 +233,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
|
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
|
||||||
)
|
)
|
||||||
self.clients[1].conn.sendall(
|
self.clients[1].conn.sendall(
|
||||||
b"METADATA * SET valid_key1 " b":invalid UTF-8: \xc3\r\n"
|
b"METADATA * SET display-name " b":invalid UTF-8: \xc3\r\n"
|
||||||
)
|
)
|
||||||
commands = {m.command for m in self.getMessages(1)}
|
commands = {m.command for m in self.getMessages(1)}
|
||||||
self.assertNotIn(
|
self.assertNotIn(
|
||||||
|
@ -1,10 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
`IRCv3 MONITOR <https://ircv3.net/specs/extensions/monitor>`_
|
`IRCv3 MONITOR <https://ircv3.net/specs/extensions/monitor>`_
|
||||||
|
and `IRCv3 extended-monitor` <https://ircv3.net/specs/extensions/extended-monitor>`_
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
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,
|
||||||
@ -13,7 +17,7 @@ from irctest.numerics import (
|
|||||||
from irctest.patma import ANYSTR, StrRe
|
from irctest.patma import ANYSTR, StrRe
|
||||||
|
|
||||||
|
|
||||||
class MonitorTestCase(cases.BaseServerTestCase):
|
class _BaseMonitorTestCase(cases.BaseServerTestCase):
|
||||||
def check_server_support(self):
|
def check_server_support(self):
|
||||||
if "MONITOR" not in self.server_support:
|
if "MONITOR" not in self.server_support:
|
||||||
raise runner.IsupportTokenNotSupported("MONITOR")
|
raise runner.IsupportTokenNotSupported("MONITOR")
|
||||||
@ -42,6 +46,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
|
|||||||
extra_format=(nick,),
|
extra_format=(nick,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MonitorTestCase(_BaseMonitorTestCase):
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.mark_isupport("MONITOR")
|
@cases.mark_isupport("MONITOR")
|
||||||
def testMonitorOneDisconnected(self):
|
def testMonitorOneDisconnected(self):
|
||||||
@ -185,14 +191,15 @@ class MonitorTestCase(cases.BaseServerTestCase):
|
|||||||
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="731")
|
self.assertMessageMatch(m, command=expected_command)
|
||||||
except NoMessageException:
|
except NoMessageException:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertMessageMatch(m, command="731")
|
self.assertMessageMatch(m, command=expected_command)
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
try:
|
try:
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
@ -244,6 +251,23 @@ class MonitorTestCase(cases.BaseServerTestCase):
|
|||||||
extra_format=(messages,),
|
extra_format=(messages,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.mark_isupport("MONITOR")
|
||||||
|
def testMonitorClear(self):
|
||||||
|
"""“Clears the list of targets being monitored. No output will be returned
|
||||||
|
for use of this command.“
|
||||||
|
-- <https://ircv3.net/specs/extensions/monitor#monitor-c>
|
||||||
|
"""
|
||||||
|
self.connectClient("foo")
|
||||||
|
self.check_server_support()
|
||||||
|
self.sendLine(1, "MONITOR + bar")
|
||||||
|
self.getMessages(1)
|
||||||
|
|
||||||
|
self.sendLine(1, "MONITOR C")
|
||||||
|
self.sendLine(1, "MONITOR L")
|
||||||
|
m = self.getMessage(1)
|
||||||
|
self.assertEqual(m.command, RPL_ENDOFMONLIST)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.mark_isupport("MONITOR")
|
@cases.mark_isupport("MONITOR")
|
||||||
def testMonitorList(self):
|
def testMonitorList(self):
|
||||||
@ -279,6 +303,35 @@ class MonitorTestCase(cases.BaseServerTestCase):
|
|||||||
self.sendLine(1, "MONITOR L")
|
self.sendLine(1, "MONITOR L")
|
||||||
checkMonitorSubjects(self.getMessages(1), "bar", {"bazbat"})
|
checkMonitorSubjects(self.getMessages(1), "bar", {"bazbat"})
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.mark_isupport("MONITOR")
|
||||||
|
def testMonitorStatus(self):
|
||||||
|
"""“Outputs for each target in the list being monitored, whether
|
||||||
|
the client is online or offline. All targets that are online will
|
||||||
|
be sent using RPL_MONONLINE, all targets that are offline will be
|
||||||
|
sent using RPL_MONOFFLINE.“
|
||||||
|
-- <https://ircv3.net/specs/extensions/monitor#monitor-s>
|
||||||
|
"""
|
||||||
|
self.connectClient("foo")
|
||||||
|
self.check_server_support()
|
||||||
|
self.connectClient("bar")
|
||||||
|
self.sendLine(1, "MONITOR + bar,baz")
|
||||||
|
self.getMessages(1)
|
||||||
|
|
||||||
|
self.sendLine(1, "MONITOR S")
|
||||||
|
msgs = self.getMessages(1)
|
||||||
|
self.assertEqual(
|
||||||
|
len(msgs),
|
||||||
|
2,
|
||||||
|
fail_msg="Expected one RPL_MONONLINE (730) and one RPL_MONOFFLINE (731), got: {}",
|
||||||
|
extra_format=(msgs,),
|
||||||
|
)
|
||||||
|
|
||||||
|
msgs.sort(key=lambda m: m.command)
|
||||||
|
|
||||||
|
self.assertMononline(1, "bar", m=msgs[0])
|
||||||
|
self.assertMonoffline(1, "baz", m=msgs[1])
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.mark_isupport("MONITOR")
|
@cases.mark_isupport("MONITOR")
|
||||||
def testNickChange(self):
|
def testNickChange(self):
|
||||||
@ -295,10 +348,11 @@ class MonitorTestCase(cases.BaseServerTestCase):
|
|||||||
self.sendLine(2, "NICK qux")
|
self.sendLine(2, "NICK qux")
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
mononline = self.getMessages(1)[0]
|
mononline = self.getMessages(1)[0]
|
||||||
self.assertEqual(mononline.command, RPL_MONONLINE)
|
self.assertMessageMatch(
|
||||||
self.assertEqual(len(mononline.params), 2, mononline.params)
|
mononline,
|
||||||
self.assertIn(mononline.params[0], ("bar", "*"))
|
command=RPL_MONONLINE,
|
||||||
self.assertEqual(mononline.params[1].split("!")[0], "qux")
|
params=[StrRe(r"(bar|\*)"), StrRe("qux(!.*)?")],
|
||||||
|
)
|
||||||
|
|
||||||
# no numerics for a case change
|
# no numerics for a case change
|
||||||
self.sendLine(2, "NICK QUX")
|
self.sendLine(2, "NICK QUX")
|
||||||
@ -309,7 +363,246 @@ class MonitorTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
monoffline = self.getMessages(1)[0]
|
monoffline = self.getMessages(1)[0]
|
||||||
# should get RPL_MONOFFLINE with the current unfolded nick
|
# should get RPL_MONOFFLINE with the current unfolded nick
|
||||||
self.assertEqual(monoffline.command, RPL_MONOFFLINE)
|
self.assertMessageMatch(
|
||||||
self.assertEqual(len(monoffline.params), 2, monoffline.params)
|
monoffline,
|
||||||
self.assertIn(monoffline.params[0], ("bar", "*"))
|
command=RPL_MONOFFLINE,
|
||||||
self.assertEqual(monoffline.params[1].split("!")[0], "QUX")
|
params=[StrRe(r"(bar|\*)"), "QUX"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _BaseExtendedMonitorTestCase(_BaseMonitorTestCase):
|
||||||
|
def _setupExtendedMonitor(self, monitor_before_connect, watcher_caps, watched_caps):
|
||||||
|
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html"""
|
||||||
|
self.connectClient(
|
||||||
|
"foo",
|
||||||
|
capabilities=["extended-monitor", *watcher_caps],
|
||||||
|
skip_if_cap_nak=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if monitor_before_connect:
|
||||||
|
self.sendLine(1, "MONITOR + bar")
|
||||||
|
self.getMessages(1)
|
||||||
|
self.connectClient("bar", capabilities=watched_caps, skip_if_cap_nak=True)
|
||||||
|
self.getMessages(2)
|
||||||
|
else:
|
||||||
|
self.connectClient("bar", capabilities=watched_caps, skip_if_cap_nak=True)
|
||||||
|
self.getMessages(2)
|
||||||
|
self.sendLine(1, "MONITOR + bar")
|
||||||
|
|
||||||
|
self.assertMononline(1, "bar")
|
||||||
|
self.assertEqual(self.getMessages(1), [])
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendedMonitorTestCase(_BaseExtendedMonitorTestCase):
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.mark_capabilities("extended-monitor", "away-notify")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"monitor_before_connect,cap",
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
monitor_before_connect,
|
||||||
|
cap,
|
||||||
|
id=("monitor_before_connect" if monitor_before_connect else "")
|
||||||
|
+ "-"
|
||||||
|
+ ("with-cap" if cap else ""),
|
||||||
|
)
|
||||||
|
for monitor_before_connect in [True, False]
|
||||||
|
for cap in [True, False]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def testExtendedMonitorAway(self, monitor_before_connect, cap):
|
||||||
|
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
||||||
|
with https://ircv3.net/specs/extensions/away-notify
|
||||||
|
"""
|
||||||
|
if cap:
|
||||||
|
self._setupExtendedMonitor(
|
||||||
|
monitor_before_connect, ["away-notify"], ["away-notify"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._setupExtendedMonitor(monitor_before_connect, ["away-notify"], [])
|
||||||
|
|
||||||
|
self.sendLine(2, "AWAY :afk")
|
||||||
|
self.getMessages(2)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(1), nick="bar", command="AWAY", params=["afk"]
|
||||||
|
)
|
||||||
|
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
||||||
|
|
||||||
|
self.sendLine(2, "AWAY")
|
||||||
|
self.getMessages(2)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(1), nick="bar", command="AWAY", params=[]
|
||||||
|
)
|
||||||
|
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.mark_capabilities("extended-monitor", "away-notify")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"monitor_before_connect,cap",
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
monitor_before_connect,
|
||||||
|
cap,
|
||||||
|
id=("monitor_before_connect" if monitor_before_connect else "")
|
||||||
|
+ "-"
|
||||||
|
+ ("with-cap" if cap else ""),
|
||||||
|
)
|
||||||
|
for monitor_before_connect in [True, False]
|
||||||
|
for cap in [True, False]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def testExtendedMonitorAwayNoCap(self, monitor_before_connect, cap):
|
||||||
|
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
||||||
|
does nothing when ``away-notify`` is not enabled by the watcher
|
||||||
|
"""
|
||||||
|
if cap:
|
||||||
|
self._setupExtendedMonitor(monitor_before_connect, [], ["away-notify"])
|
||||||
|
else:
|
||||||
|
self._setupExtendedMonitor(monitor_before_connect, [], [])
|
||||||
|
|
||||||
|
self.sendLine(2, "AWAY :afk")
|
||||||
|
self.getMessages(2)
|
||||||
|
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
||||||
|
|
||||||
|
self.sendLine(2, "AWAY")
|
||||||
|
self.getMessages(2)
|
||||||
|
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.mark_capabilities("extended-monitor", "setname")
|
||||||
|
@pytest.mark.parametrize("monitor_before_connect", [True, False])
|
||||||
|
def testExtendedMonitorSetName(self, monitor_before_connect):
|
||||||
|
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
||||||
|
with https://ircv3.net/specs/extensions/setname
|
||||||
|
"""
|
||||||
|
self._setupExtendedMonitor(monitor_before_connect, ["setname"], ["setname"])
|
||||||
|
|
||||||
|
self.sendLine(2, "SETNAME :new name")
|
||||||
|
self.getMessages(2)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(1), nick="bar", command="SETNAME", params=["new name"]
|
||||||
|
)
|
||||||
|
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.mark_capabilities("extended-monitor", "setname")
|
||||||
|
@pytest.mark.parametrize("monitor_before_connect", [True, False])
|
||||||
|
def testExtendedMonitorSetNameNoCap(self, monitor_before_connect):
|
||||||
|
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
||||||
|
does nothing when ``setname`` is not enabled by the watcher
|
||||||
|
"""
|
||||||
|
self._setupExtendedMonitor(monitor_before_connect, [], ["setname"])
|
||||||
|
|
||||||
|
self.sendLine(2, "SETNAME :new name")
|
||||||
|
self.getMessages(2)
|
||||||
|
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
||||||
|
|
||||||
|
|
||||||
|
@cases.mark_services
|
||||||
|
class AuthenticatedExtendedMonitorTestCase(_BaseExtendedMonitorTestCase):
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.mark_capabilities("extended-monitor", "account-notify")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"monitor_before_connect,cap",
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
monitor_before_connect,
|
||||||
|
cap,
|
||||||
|
id=("monitor_before_connect" if monitor_before_connect else "")
|
||||||
|
+ "-"
|
||||||
|
+ ("with-cap" if cap else ""),
|
||||||
|
)
|
||||||
|
for monitor_before_connect in [True, False]
|
||||||
|
for cap in [True, False]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def testExtendedMonitorAccountNotify(self, monitor_before_connect, cap):
|
||||||
|
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
||||||
|
does nothing when ``account-notify`` is not enabled by the watcher
|
||||||
|
"""
|
||||||
|
self.controller.registerUser(self, "jilles", "sesame")
|
||||||
|
|
||||||
|
if cap:
|
||||||
|
self._setupExtendedMonitor(
|
||||||
|
monitor_before_connect,
|
||||||
|
["account-notify"],
|
||||||
|
["account-notify", "sasl", "cap-notify"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._setupExtendedMonitor(
|
||||||
|
monitor_before_connect, ["account-notify"], ["sasl", "cap-notify"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(2, "AUTHENTICATE PLAIN")
|
||||||
|
m = self.getRegistrationMessage(2)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="AUTHENTICATE",
|
||||||
|
params=["+"],
|
||||||
|
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
|
||||||
|
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
|
||||||
|
)
|
||||||
|
self.sendLine(2, "AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=")
|
||||||
|
m = self.getRegistrationMessage(2)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="900",
|
||||||
|
fail_msg="Did not send 900 after correct SASL authentication.",
|
||||||
|
)
|
||||||
|
self.getMessages(2)
|
||||||
|
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(1), nick="bar", command="ACCOUNT", params=["jilles"]
|
||||||
|
)
|
||||||
|
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.mark_capabilities("extended-monitor", "account-notify")
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"monitor_before_connect,cap",
|
||||||
|
[
|
||||||
|
pytest.param(
|
||||||
|
monitor_before_connect,
|
||||||
|
cap,
|
||||||
|
id=("monitor_before_connect" if monitor_before_connect else "")
|
||||||
|
+ "-"
|
||||||
|
+ ("with-cap" if cap else ""),
|
||||||
|
)
|
||||||
|
for monitor_before_connect in [True, False]
|
||||||
|
for cap in [True, False]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def testExtendedMonitorAccountNotifyNoCap(self, monitor_before_connect, cap):
|
||||||
|
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
||||||
|
does nothing when ``account-notify`` is not enabled by the watcher
|
||||||
|
"""
|
||||||
|
self.controller.registerUser(self, "jilles", "sesame")
|
||||||
|
|
||||||
|
if cap:
|
||||||
|
self._setupExtendedMonitor(
|
||||||
|
monitor_before_connect, [], ["account-notify", "sasl", "cap-notify"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._setupExtendedMonitor(
|
||||||
|
monitor_before_connect, [], ["sasl", "cap-notify"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(2, "AUTHENTICATE PLAIN")
|
||||||
|
m = self.getRegistrationMessage(2)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="AUTHENTICATE",
|
||||||
|
params=["+"],
|
||||||
|
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
|
||||||
|
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
|
||||||
|
)
|
||||||
|
self.sendLine(2, "AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=")
|
||||||
|
m = self.getRegistrationMessage(2)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="900",
|
||||||
|
fail_msg="Did not send 900 after correct SASL authentication.",
|
||||||
|
)
|
||||||
|
self.getMessages(2)
|
||||||
|
|
||||||
|
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
||||||
|
@ -15,7 +15,7 @@ class MultiPrefixTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
These prefixes MUST be in order of ‘rank’, from highest to lowest.
|
These prefixes MUST be in order of ‘rank’, from highest to lowest.
|
||||||
"""
|
"""
|
||||||
self.connectClient("foo", capabilities=["multi-prefix"])
|
self.connectClient("foo", capabilities=["multi-prefix"], skip_if_cap_nak=True)
|
||||||
self.joinChannel(1, "#chan")
|
self.joinChannel(1, "#chan")
|
||||||
self.sendLine(1, "MODE #chan +v foo")
|
self.sendLine(1, "MODE #chan +v foo")
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
@ -24,11 +24,6 @@ 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",
|
||||||
@ -47,9 +42,57 @@ 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.assertTrue(
|
self.assertIn(
|
||||||
"@+" 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, StrRe
|
from irctest.patma import ANYDICT, ANYSTR, StrRe
|
||||||
|
|
||||||
CAP_NAME = "draft/multiline"
|
CAP_NAME = "draft/multiline"
|
||||||
BATCH_TYPE = "draft/multiline"
|
BATCH_TYPE = "draft/multiline"
|
||||||
@ -135,3 +135,86 @@ 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],
|
||||||
|
)
|
||||||
|
@ -11,7 +11,7 @@ from irctest.patma import ANYSTR, StrRe
|
|||||||
|
|
||||||
|
|
||||||
class NamesTestCase(cases.BaseServerTestCase):
|
class NamesTestCase(cases.BaseServerTestCase):
|
||||||
def _testNames(self, symbol):
|
def _testNames(self, symbol: bool, allow_trailing_space: bool):
|
||||||
self.connectClient("nick1")
|
self.connectClient("nick1")
|
||||||
self.sendLine(1, "JOIN #chan")
|
self.sendLine(1, "JOIN #chan")
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
@ -31,7 +31,10 @@ class NamesTestCase(cases.BaseServerTestCase):
|
|||||||
"nick1",
|
"nick1",
|
||||||
*(["="] if symbol else []),
|
*(["="] if symbol else []),
|
||||||
"#chan",
|
"#chan",
|
||||||
StrRe("(nick2 @nick1|@nick1 nick2)"),
|
StrRe(
|
||||||
|
"(nick2 @nick1|@nick1 nick2)"
|
||||||
|
+ (" ?" if allow_trailing_space else "")
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -44,20 +47,59 @@ 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)
|
self._testNames(symbol=False, allow_trailing_space=True)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
@cases.mark_specifications("RFC2812", "Modern")
|
||||||
def testNames2812(self):
|
def testNames2812(self):
|
||||||
"""
|
"""
|
||||||
https://modern.ircdocs.horse/#names-message
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
||||||
"""
|
"""
|
||||||
self._testNames(symbol=True)
|
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
|
||||||
|
"""
|
||||||
|
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://modern.ircdocs.horse/#rplnamreply-353
|
||||||
|
"""
|
||||||
|
self.connectClient("nick1")
|
||||||
|
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")
|
||||||
|
@ -10,6 +10,7 @@ TODO: cross-reference Modern
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
|
from irctest.numerics import RPL_NAMREPLY
|
||||||
|
|
||||||
|
|
||||||
class PartTestCase(cases.BaseServerTestCase):
|
class PartTestCase(cases.BaseServerTestCase):
|
||||||
@ -84,6 +85,12 @@ class PartTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
|
|
||||||
|
self.sendLine(2, "PRIVMSG #chan :hi everyone")
|
||||||
|
self.getMessages(2)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(1), command="PRIVMSG", params=["#chan", "hi everyone"]
|
||||||
|
)
|
||||||
|
|
||||||
self.sendLine(1, "PART #chan")
|
self.sendLine(1, "PART #chan")
|
||||||
# both the PART'ing client and the other channel member should receive
|
# both the PART'ing client and the other channel member should receive
|
||||||
# a PART line:
|
# a PART line:
|
||||||
@ -92,6 +99,21 @@ class PartTestCase(cases.BaseServerTestCase):
|
|||||||
m = self.getMessage(2)
|
m = self.getMessage(2)
|
||||||
self.assertMessageMatch(m, command="PART")
|
self.assertMessageMatch(m, command="PART")
|
||||||
|
|
||||||
|
self.sendLine(2, "PRIVMSG #chan :hi again everyone")
|
||||||
|
self.getMessages(2)
|
||||||
|
# client 1 has PART'ed and should not receive channel messages:
|
||||||
|
self.assertEqual(self.getMessages(1), [])
|
||||||
|
|
||||||
|
# client 1 should no longer appear in NAMES responses:
|
||||||
|
names = set()
|
||||||
|
self.sendLine(2, "NAMES #chan")
|
||||||
|
for reply in self.getMessages(2):
|
||||||
|
if reply.command != RPL_NAMREPLY:
|
||||||
|
continue
|
||||||
|
names.update(reply.params[-1].replace("@", "").split())
|
||||||
|
self.assertNotIn("bar", names)
|
||||||
|
self.assertIn("baz", names)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812")
|
@cases.mark_specifications("RFC2812")
|
||||||
def testBasicPartRfc2812(self):
|
def testBasicPartRfc2812(self):
|
||||||
"""
|
"""
|
||||||
|
@ -10,7 +10,6 @@ TODO: cross-reference RFC 1459 and Modern
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import ERR_CANNOTSENDTOCHAN
|
|
||||||
from irctest.patma import StrRe
|
from irctest.patma import StrRe
|
||||||
|
|
||||||
|
|
||||||
@ -40,31 +39,3 @@ class ChannelQuitTestCase(cases.BaseServerTestCase):
|
|||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertMessageMatch(m, command="QUIT", params=[StrRe(".*qux out.*")])
|
self.assertMessageMatch(m, command="QUIT", params=[StrRe(".*qux out.*")])
|
||||||
self.assertTrue(m.prefix.startswith("qux")) # nickmask of quitter
|
self.assertTrue(m.prefix.startswith("qux")) # nickmask of quitter
|
||||||
|
|
||||||
|
|
||||||
class NoCTCPTestCase(cases.BaseServerTestCase):
|
|
||||||
@cases.mark_specifications("Ergo")
|
|
||||||
def testQuit(self):
|
|
||||||
self.connectClient("bar")
|
|
||||||
self.joinChannel(1, "#chan")
|
|
||||||
self.sendLine(1, "MODE #chan +C")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.connectClient("qux")
|
|
||||||
self.joinChannel(2, "#chan")
|
|
||||||
self.getMessages(2)
|
|
||||||
|
|
||||||
self.sendLine(1, "PRIVMSG #chan :\x01ACTION hi\x01")
|
|
||||||
self.getMessages(1)
|
|
||||||
ms = self.getMessages(2)
|
|
||||||
self.assertEqual(len(ms), 1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
ms[0], command="PRIVMSG", params=["#chan", "\x01ACTION hi\x01"]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "PRIVMSG #chan :\x01PING 1473523796 918320\x01")
|
|
||||||
ms = self.getMessages(1)
|
|
||||||
self.assertEqual(len(ms), 1)
|
|
||||||
self.assertMessageMatch(ms[0], command=ERR_CANNOTSENDTOCHAN)
|
|
||||||
ms = self.getMessages(2)
|
|
||||||
self.assertEqual(ms, [])
|
|
||||||
|
@ -42,14 +42,21 @@ class RegressionsTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
|
|
||||||
# case change: both alice and bob should get a successful nick line
|
# 'alice' is claimed, so 'Alice' is reserved and Bob cannot take it:
|
||||||
|
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], command="NICK", params=["Alice"])
|
self.assertMessageMatch(ms[0], nick="alice", 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], command="NICK", params=["Alice"])
|
self.assertMessageMatch(ms[0], nick="alice", 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")
|
||||||
@ -190,3 +197,27 @@ 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
|
from irctest.numerics import ERR_SASLFAIL, RPL_LOGGEDIN, RPL_SASLMECHS
|
||||||
from irctest.patma import ANYSTR
|
from irctest.patma import ANYSTR
|
||||||
|
|
||||||
|
|
||||||
@ -48,11 +48,37 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||||||
m = self.getRegistrationMessage(1)
|
m = self.getRegistrationMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
command="900",
|
command=RPL_LOGGEDIN,
|
||||||
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):
|
||||||
@ -161,11 +187,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 == "908": # RPL_SASLMECHS
|
while m.command == RPL_SASLMECHS:
|
||||||
m = self.getRegistrationMessage(1)
|
m = self.getRegistrationMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
command="904",
|
command=ERR_SASLFAIL,
|
||||||
fail_msg="Did not reply with 904 to “AUTHENTICATE FOO”: {msg}",
|
fail_msg="Did not reply with 904 to “AUTHENTICATE FOO”: {msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -178,6 +204,14 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||||||
),
|
),
|
||||||
"Anope does not handle split AUTHENTICATE (reported on IRC)",
|
"Anope does not handle split AUTHENTICATE (reported on IRC)",
|
||||||
)
|
)
|
||||||
|
@cases.xfailIf(
|
||||||
|
lambda self: (
|
||||||
|
self.controller.services_controller is not None
|
||||||
|
and self.controller.services_controller.software_name == "Dlk-Services"
|
||||||
|
),
|
||||||
|
"Dlk does not handle split AUTHENTICATE "
|
||||||
|
"https://github.com/DalekIRC/Dalek-Services/issues/28",
|
||||||
|
)
|
||||||
def testPlainLarge(self):
|
def testPlainLarge(self):
|
||||||
"""Test the client splits large AUTHENTICATE messages whose payload
|
"""Test the client splits large AUTHENTICATE messages whose payload
|
||||||
is not a multiple of 400.
|
is not a multiple of 400.
|
||||||
|
65
irctest/server_tests/setname.py
Normal file
65
irctest/server_tests/setname.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
`IRCv3 SETNAME<https://ircv3.net/specs/extensions/setname>`_
|
||||||
|
"""
|
||||||
|
|
||||||
|
from irctest import cases
|
||||||
|
from irctest.numerics import RPL_WHOISUSER
|
||||||
|
|
||||||
|
|
||||||
|
class SetnameMessageTestCase(cases.BaseServerTestCase):
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.mark_capabilities("setname")
|
||||||
|
def testSetnameMessage(self):
|
||||||
|
self.connectClient("foo", capabilities=["setname"], skip_if_cap_nak=True)
|
||||||
|
|
||||||
|
self.sendLine(1, "SETNAME bar")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(1),
|
||||||
|
command="SETNAME",
|
||||||
|
params=["bar"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, "WHOIS foo")
|
||||||
|
whoisuser = [m for m in self.getMessages(1) if m.command == RPL_WHOISUSER][0]
|
||||||
|
self.assertEqual(whoisuser.params[-1], "bar")
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
@cases.mark_capabilities("setname")
|
||||||
|
def testSetnameChannel(self):
|
||||||
|
"""“[Servers] MUST send the server-to-client version of the
|
||||||
|
SETNAME message to all clients in common channels, as well as
|
||||||
|
to the client from which it originated, to confirm the change
|
||||||
|
has occurred.
|
||||||
|
|
||||||
|
The SETNAME message MUST NOT be sent to clients which do not
|
||||||
|
have the setname capability negotiated.“
|
||||||
|
"""
|
||||||
|
self.connectClient("foo", capabilities=["setname"], skip_if_cap_nak=True)
|
||||||
|
self.connectClient("bar", capabilities=["setname"], skip_if_cap_nak=True)
|
||||||
|
self.connectClient("baz")
|
||||||
|
|
||||||
|
self.joinChannel(1, "#chan")
|
||||||
|
self.joinChannel(2, "#chan")
|
||||||
|
self.joinChannel(3, "#chan")
|
||||||
|
self.getMessages(1)
|
||||||
|
self.getMessages(2)
|
||||||
|
self.getMessages(3)
|
||||||
|
|
||||||
|
self.sendLine(1, "SETNAME qux")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(1),
|
||||||
|
command="SETNAME",
|
||||||
|
params=["qux"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(2),
|
||||||
|
command="SETNAME",
|
||||||
|
params=["qux"],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.getMessages(3),
|
||||||
|
[],
|
||||||
|
"Got SETNAME response when it was not negotiated",
|
||||||
|
)
|
@ -11,13 +11,29 @@ from irctest.numerics import ERR_CHANOPRIVSNEEDED, RPL_NOTOPIC, RPL_TOPIC, RPL_T
|
|||||||
|
|
||||||
class TopicTestCase(cases.BaseServerTestCase):
|
class TopicTestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812")
|
@cases.mark_specifications("RFC1459", "RFC2812")
|
||||||
def testTopic(self):
|
def testTopicRfc(self):
|
||||||
"""“Once a user has joined a channel, he receives information about
|
"""“Once a user has joined a channel, he receives information about
|
||||||
all commands his server receives affecting the channel. This
|
all commands his server receives affecting the channel. This
|
||||||
includes […] TOPIC”
|
includes […] TOPIC”
|
||||||
-- <https://tools.ietf.org/html/rfc1459#section-4.2.1>
|
-- <https://tools.ietf.org/html/rfc1459#section-4.2.1>
|
||||||
and <https://tools.ietf.org/html/rfc2812#section-3.2.1>
|
and <https://tools.ietf.org/html/rfc2812#section-3.2.1>
|
||||||
"""
|
"""
|
||||||
|
self._testTopic(assert_echo=False)
|
||||||
|
|
||||||
|
@cases.mark_specifications("Modern")
|
||||||
|
def testTopicModern(self):
|
||||||
|
""" "If the topic of a channel is changed or cleared, every client in that
|
||||||
|
channel (including the author of the topic change) will receive a TOPIC command
|
||||||
|
with the new topic as argument (or an empty argument if the topic was cleared)
|
||||||
|
alerting them to how the topic has changed.
|
||||||
|
|
||||||
|
Clients joining the channel in the future will receive a RPL_TOPIC numeric (or
|
||||||
|
lack thereof) accordingly."
|
||||||
|
-- https://modern.ircdocs.horse/#topic-message
|
||||||
|
"""
|
||||||
|
self._testTopic(assert_echo=True)
|
||||||
|
|
||||||
|
def _testTopic(self, assert_echo: bool):
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.joinChannel(1, "#chan")
|
self.joinChannel(1, "#chan")
|
||||||
|
|
||||||
@ -41,6 +57,7 @@ class TopicTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
self.assertMessageMatch(m, command="TOPIC")
|
self.assertMessageMatch(m, command="TOPIC")
|
||||||
except client_mock.NoMessageException:
|
except client_mock.NoMessageException:
|
||||||
|
self.assertFalse(assert_echo, "TOPIC was not echoed back to the author")
|
||||||
# The RFCs do not say TOPIC must be echoed
|
# The RFCs do not say TOPIC must be echoed
|
||||||
pass
|
pass
|
||||||
m = self.getMessage(2)
|
m = self.getMessage(2)
|
||||||
|
@ -1,36 +1,22 @@
|
|||||||
"""
|
"""
|
||||||
`Ergo <https://ergo.chat/>`_-specific tests of non-Unicode filtering
|
`Ergo <https://ergo.chat/>`_-specific tests of non-Unicode filtering
|
||||||
|
|
||||||
TODO: turn this into a test of `IRCv3 UTF8ONLY
|
|
||||||
<https://ircv3.net/specs/extensions/utf8-only>`_
|
<https://ircv3.net/specs/extensions/utf8-only>`_
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases, runner
|
||||||
|
from irctest.numerics import ERR_ERRONEUSNICKNAME
|
||||||
from irctest.patma import ANYSTR
|
from irctest.patma import ANYSTR
|
||||||
|
|
||||||
|
|
||||||
class Utf8TestCase(cases.BaseServerTestCase):
|
class Utf8TestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_specifications("Ergo")
|
@cases.mark_specifications("Ergo")
|
||||||
def testUtf8Validation(self):
|
def testNonUtf8Filtering(self):
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
"bar",
|
"bar",
|
||||||
capabilities=["batch", "echo-message", "labeled-response"],
|
capabilities=["batch", "echo-message", "labeled-response"],
|
||||||
)
|
)
|
||||||
self.joinChannel(1, "#qux")
|
self.joinChannel(1, "#qux")
|
||||||
self.sendLine(1, "PRIVMSG #qux hi")
|
|
||||||
ms = self.getMessages(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
[m for m in ms if m.command == "PRIVMSG"][0], params=["#qux", "hi"]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, b"PRIVMSG #qux hi\xaa")
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command="FAIL",
|
|
||||||
params=["PRIVMSG", "INVALID_UTF8", ANYSTR],
|
|
||||||
tags={},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, b"@label=xyz PRIVMSG #qux hi\xaa")
|
self.sendLine(1, b"@label=xyz PRIVMSG #qux hi\xaa")
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
self.getMessage(1),
|
self.getMessage(1),
|
||||||
@ -38,3 +24,111 @@ class Utf8TestCase(cases.BaseServerTestCase):
|
|||||||
params=["PRIVMSG", "INVALID_UTF8", ANYSTR],
|
params=["PRIVMSG", "INVALID_UTF8", ANYSTR],
|
||||||
tags={"label": "xyz"},
|
tags={"label": "xyz"},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cases.mark_isupport("UTF8ONLY")
|
||||||
|
def testUtf8Validation(self):
|
||||||
|
self.connectClient("foo")
|
||||||
|
self.connectClient("bar")
|
||||||
|
|
||||||
|
if "UTF8ONLY" not in self.server_support:
|
||||||
|
raise runner.IsupportTokenNotSupported("UTF8ONLY")
|
||||||
|
|
||||||
|
self.sendLine(1, "PRIVMSG bar hi")
|
||||||
|
self.getMessages(1) # synchronize
|
||||||
|
ms = self.getMessages(2)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
[m for m in ms if m.command == "PRIVMSG"][0], params=["bar", "hi"]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.sendLine(1, b"PRIVMSG bar hi\xaa")
|
||||||
|
|
||||||
|
m = self.getMessage(1)
|
||||||
|
assert m.command in ("FAIL", "WARN", "ERROR")
|
||||||
|
|
||||||
|
if m.command in ("FAIL", "WARN"):
|
||||||
|
self.assertMessageMatch(m, params=["PRIVMSG", "INVALID_UTF8", ANYSTR])
|
||||||
|
|
||||||
|
def testNonutf8Realname(self):
|
||||||
|
self.connectClient("foo")
|
||||||
|
if "UTF8ONLY" not in self.server_support:
|
||||||
|
raise runner.IsupportTokenNotSupported("UTF8ONLY")
|
||||||
|
|
||||||
|
self.addClient()
|
||||||
|
self.sendLine(2, "NICK bar")
|
||||||
|
self.clients[2].conn.sendall(b"USER username * * :i\xe8rc\xe9\r\n")
|
||||||
|
|
||||||
|
d = b""
|
||||||
|
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
|
||||||
|
self.assertIn(b"001 ", d)
|
||||||
|
|
||||||
|
self.sendLine(2, "WHOIS bar")
|
||||||
|
self.getMessages(2)
|
||||||
|
|
||||||
|
def testNonutf8Username(self):
|
||||||
|
self.connectClient("foo")
|
||||||
|
if "UTF8ONLY" not in self.server_support:
|
||||||
|
raise runner.IsupportTokenNotSupported("UTF8ONLY")
|
||||||
|
|
||||||
|
self.addClient()
|
||||||
|
self.sendLine(2, "NICK bar")
|
||||||
|
self.clients[2].conn.sendall(b"USER \xe8rc\xe9 * * :readlname\r\n")
|
||||||
|
|
||||||
|
d = b""
|
||||||
|
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
|
||||||
|
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.getMessage(2), nick="Işıl", params=["#test", "hi there"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
@ -37,8 +37,8 @@ class BaseWhoTestCase:
|
|||||||
self.sendLine(1, f"USER {self.username} 0 * :{self.realname}")
|
self.sendLine(1, f"USER {self.username} 0 * :{self.realname}")
|
||||||
if auth:
|
if auth:
|
||||||
self.sendLine(1, "CAP END")
|
self.sendLine(1, "CAP END")
|
||||||
self.getRegistrationMessage(1)
|
|
||||||
self.skipToWelcome(1)
|
self.skipToWelcome(1)
|
||||||
|
self.getMessages(1)
|
||||||
self.sendLine(1, "JOIN #chan")
|
self.sendLine(1, "JOIN #chan")
|
||||||
|
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
@ -60,7 +60,7 @@ class BaseWhoTestCase:
|
|||||||
"*", # no chan
|
"*", # no chan
|
||||||
StrRe("~?" + self.username),
|
StrRe("~?" + self.username),
|
||||||
StrRe(host_re),
|
StrRe(host_re),
|
||||||
"My.Little.Server",
|
StrRe(r"(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),
|
||||||
"My.Little.Server",
|
StrRe(r"(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 == "Bahamut":
|
if self.controller.software_name in ("Bahamut",):
|
||||||
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 == "Bahamut":
|
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
||||||
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 == "Bahamut":
|
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
||||||
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 == "Bahamut":
|
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self._init()
|
self._init()
|
||||||
@ -228,9 +228,14 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"]
|
"mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"]
|
||||||
)
|
)
|
||||||
|
@cases.xfailIfSoftware(
|
||||||
|
["Sable"],
|
||||||
|
"Sable does not advertise oper status in WHO: "
|
||||||
|
"https://github.com/Libera-Chat/sable/pull/77",
|
||||||
|
)
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testWhoNickOper(self, mask):
|
def testWhoNickOper(self, mask):
|
||||||
if "*" in mask and self.controller.software_name == "Bahamut":
|
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self._init()
|
self._init()
|
||||||
@ -262,9 +267,14 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"]
|
"mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"]
|
||||||
)
|
)
|
||||||
|
@cases.xfailIfSoftware(
|
||||||
|
["Sable"],
|
||||||
|
"Sable does not advertise oper status in WHO: "
|
||||||
|
"https://github.com/Libera-Chat/sable/pull/77",
|
||||||
|
)
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testWhoNickAwayAndOper(self, mask):
|
def testWhoNickAwayAndOper(self, mask):
|
||||||
if "*" in mask and self.controller.software_name == "Bahamut":
|
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self._init()
|
self._init()
|
||||||
@ -298,18 +308,11 @@ 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 == "Bahamut":
|
if "*" in mask and self.controller.software_name in ("Bahamut",):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self._init()
|
self._init()
|
||||||
|
|
||||||
self.sendLine(1, "OPER operuser operpassword")
|
|
||||||
self.assertIn(
|
|
||||||
RPL_YOUREOPER,
|
|
||||||
[m.command for m in self.getMessages(1)],
|
|
||||||
fail_msg="OPER failed",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "AWAY :be right back")
|
self.sendLine(1, "AWAY :be right back")
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
@ -333,9 +336,9 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
"#chan",
|
"#chan",
|
||||||
StrRe("~?" + self.username),
|
StrRe("~?" + self.username),
|
||||||
StrRe(host_re),
|
StrRe(host_re),
|
||||||
"My.Little.Server",
|
StrRe(r"(My.Little.Server|\*)"),
|
||||||
"coolNick",
|
"coolNick",
|
||||||
"G*@",
|
"G@",
|
||||||
StrRe(realname_regexp(self.realname)),
|
StrRe(realname_regexp(self.realname)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -348,7 +351,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
"#chan",
|
"#chan",
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
"My.Little.Server",
|
StrRe(r"(My.Little.Server|\*)"),
|
||||||
"otherNick",
|
"otherNick",
|
||||||
"H",
|
"H",
|
||||||
StrRe("[0-9]+ .*"),
|
StrRe("[0-9]+ .*"),
|
||||||
@ -361,6 +364,87 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
params=["otherNick", InsensitiveStr(mask), ANYSTR],
|
params=["otherNick", InsensitiveStr(mask), ANYSTR],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("Modern")
|
||||||
|
def testWhoMultiChan(self):
|
||||||
|
"""
|
||||||
|
When WHO <#chan> is sent, the second parameter of RPL_WHOREPLY must
|
||||||
|
be ``#chan``. See discussion on Modern:
|
||||||
|
<https://github.com/ircdocs/modern-irc/issues/209>
|
||||||
|
"""
|
||||||
|
self._init()
|
||||||
|
|
||||||
|
self.sendLine(1, "JOIN #otherchan")
|
||||||
|
self.getMessages(1)
|
||||||
|
|
||||||
|
self.sendLine(2, "JOIN #otherchan")
|
||||||
|
self.getMessages(2)
|
||||||
|
|
||||||
|
for chan in ["#chan", "#otherchan"]:
|
||||||
|
self.sendLine(2, f"WHO {chan}")
|
||||||
|
messages = self.getMessages(2)
|
||||||
|
|
||||||
|
self.assertEqual(len(messages), 3, "Unexpected number of messages")
|
||||||
|
|
||||||
|
(*replies, end) = messages
|
||||||
|
|
||||||
|
# Get them in deterministic order
|
||||||
|
replies.sort(key=lambda msg: msg.params[5])
|
||||||
|
|
||||||
|
self.assertMessageMatch(
|
||||||
|
replies[0],
|
||||||
|
command=RPL_WHOREPLY,
|
||||||
|
params=[
|
||||||
|
"otherNick",
|
||||||
|
chan,
|
||||||
|
ANYSTR,
|
||||||
|
ANYSTR,
|
||||||
|
StrRe(r"(My.Little.Server|\*)"),
|
||||||
|
"coolNick",
|
||||||
|
ANYSTR,
|
||||||
|
ANYSTR,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertMessageMatch(
|
||||||
|
replies[1],
|
||||||
|
command=RPL_WHOREPLY,
|
||||||
|
params=[
|
||||||
|
"otherNick",
|
||||||
|
chan,
|
||||||
|
ANYSTR,
|
||||||
|
ANYSTR,
|
||||||
|
StrRe(r"(My.Little.Server|\*)"),
|
||||||
|
"otherNick",
|
||||||
|
ANYSTR,
|
||||||
|
ANYSTR,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertMessageMatch(
|
||||||
|
end,
|
||||||
|
command=RPL_ENDOFWHO,
|
||||||
|
params=["otherNick", InsensitiveStr(chan), ANYSTR],
|
||||||
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("Modern")
|
||||||
|
def testWhoNickNotExists(self):
|
||||||
|
"""
|
||||||
|
When WHO is sent with a non-existing nickname, the server must reply
|
||||||
|
with a single RPL_ENDOFWHO. See:
|
||||||
|
<https://github.com/ircdocs/modern-irc/pull/216>
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._init()
|
||||||
|
|
||||||
|
self.sendLine(2, "WHO idontexist")
|
||||||
|
(end,) = self.getMessages(2)
|
||||||
|
|
||||||
|
self.assertMessageMatch(
|
||||||
|
end,
|
||||||
|
command=RPL_ENDOFWHO,
|
||||||
|
params=["otherNick", InsensitiveStr("idontexist"), ANYSTR],
|
||||||
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.mark_isupport("WHOX")
|
@cases.mark_isupport("WHOX")
|
||||||
def testWhoxFull(self):
|
def testWhoxFull(self):
|
||||||
@ -395,7 +479,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
StrRe("~?myusernam"),
|
StrRe("~?myusernam"),
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
ANYSTR,
|
ANYSTR,
|
||||||
"My.Little.Server",
|
StrRe(r"(My.Little.Server|\*)"),
|
||||||
"coolNick",
|
"coolNick",
|
||||||
StrRe("H@?"),
|
StrRe("H@?"),
|
||||||
ANYSTR, # hopcount
|
ANYSTR, # hopcount
|
||||||
@ -412,6 +496,46 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
|
|||||||
params=["otherNick", InsensitiveStr("coolNick"), ANYSTR],
|
params=["otherNick", InsensitiveStr("coolNick"), ANYSTR],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("char", "cuihsnfdlaor")
|
||||||
|
@cases.xfailIf(
|
||||||
|
lambda self, char: bool(
|
||||||
|
char == "l" and self.controller.software_name == "ircu2"
|
||||||
|
),
|
||||||
|
"https://github.com/UndernetIRC/ircu2/commit/17c539103abbd0055b2297e17854cd0756c85d62",
|
||||||
|
)
|
||||||
|
@cases.xfailIf(
|
||||||
|
lambda self, char: bool(
|
||||||
|
char == "l" and self.controller.software_name == "Nefarious"
|
||||||
|
),
|
||||||
|
"https://github.com/evilnet/nefarious2/pull/73",
|
||||||
|
)
|
||||||
|
def testWhoxOneChar(self, char):
|
||||||
|
self._init()
|
||||||
|
if "WHOX" not in self.server_support:
|
||||||
|
raise runner.IsupportTokenNotSupported("WHOX")
|
||||||
|
|
||||||
|
self.sendLine(2, f"WHO coolNick %{char}")
|
||||||
|
messages = self.getMessages(2)
|
||||||
|
|
||||||
|
self.assertEqual(len(messages), 2, "Unexpected number of messages")
|
||||||
|
|
||||||
|
(reply, end) = messages
|
||||||
|
|
||||||
|
self.assertMessageMatch(
|
||||||
|
reply,
|
||||||
|
command=RPL_WHOSPCRPL,
|
||||||
|
params=[
|
||||||
|
"otherNick",
|
||||||
|
StrRe(".+"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertMessageMatch(
|
||||||
|
end,
|
||||||
|
command=RPL_ENDOFWHO,
|
||||||
|
params=["otherNick", InsensitiveStr("coolNick"), ANYSTR],
|
||||||
|
)
|
||||||
|
|
||||||
def testWhoxToken(self):
|
def testWhoxToken(self):
|
||||||
"""https://github.com/ircv3/ircv3-specifications/pull/482"""
|
"""https://github.com/ircv3/ircv3-specifications/pull/482"""
|
||||||
self._init()
|
self._init()
|
||||||
@ -508,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 == "Bahamut":
|
if self.controller.software_name in ("Bahamut",):
|
||||||
raise runner.OptionalExtensionNotSupported("WHO mask")
|
raise runner.OptionalExtensionNotSupported("WHO mask")
|
||||||
|
|
||||||
self.connectClient("evan", name="evan")
|
self.connectClient("evan", name="evan")
|
||||||
|
@ -8,6 +8,7 @@ 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,
|
||||||
@ -56,6 +57,7 @@ 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")
|
||||||
|
|
||||||
@ -71,7 +73,10 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
|
|||||||
last_message,
|
last_message,
|
||||||
command=RPL_ENDOFWHOIS,
|
command=RPL_ENDOFWHOIS,
|
||||||
params=["nick1", "nick2", ANYSTR],
|
params=["nick1", "nick2", ANYSTR],
|
||||||
fail_msg=f"Last message was not RPL_ENDOFWHOIS ({RPL_ENDOFWHOIS})",
|
fail_msg=(
|
||||||
|
f"Expected RPL_ENDOFWHOIS ({RPL_ENDOFWHOIS}) as last message, "
|
||||||
|
f"got {{msg}}"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
unexpected_messages = []
|
unexpected_messages = []
|
||||||
@ -92,10 +97,18 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
|
|||||||
params=[
|
params=[
|
||||||
"nick1",
|
"nick1",
|
||||||
"nick2",
|
"nick2",
|
||||||
StrRe("(@#chan1 @#chan2|@#chan2 @#chan1)"),
|
# trailing space was required by the RFCs, and Modern explicitly
|
||||||
|
# allows it
|
||||||
|
StrRe("(@#chan1 @#chan2|@#chan2 @#chan1) ?"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
elif m.command == RPL_WHOISSPECIAL:
|
elif m.command == RPL_WHOISSPECIAL:
|
||||||
|
services_controller = self.controller.services_controller
|
||||||
|
if (
|
||||||
|
services_controller is not None
|
||||||
|
and services_controller.software_name == "Dlk-Services"
|
||||||
|
):
|
||||||
|
continue
|
||||||
# Technically allowed, but it's a bad style to use this without
|
# Technically allowed, but it's a bad style to use this without
|
||||||
# explicit configuration by the operators.
|
# explicit configuration by the operators.
|
||||||
assert False, "RPL_WHOISSPECIAL in use with default configuration"
|
assert False, "RPL_WHOISSPECIAL in use with default configuration"
|
||||||
@ -186,18 +199,44 @@ class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase):
|
|||||||
|
|
||||||
self.connectClient("otherNick")
|
self.connectClient("otherNick")
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
self.sendLine(2, f"WHOIS {server} coolnick")
|
self.sendLine(2, f"WHOIS {server} {nick}")
|
||||||
messages = self.getMessages(2)
|
messages = self.getMessages(2)
|
||||||
whois_user = messages[0]
|
whois_user = messages[0]
|
||||||
self.assertEqual(whois_user.command, RPL_WHOISUSER)
|
self.assertMessageMatch(
|
||||||
# "<client> <nick> <username> <host> * :<realname>"
|
whois_user,
|
||||||
self.assertEqual(whois_user.params[1], nick)
|
command=RPL_WHOISUSER,
|
||||||
self.assertIn(whois_user.params[2], ("~" + username, username))
|
# "<client> <nick> <username> <host> * :<realname>"
|
||||||
|
params=[
|
||||||
|
"otherNick",
|
||||||
|
nick,
|
||||||
|
StrRe("~?" + username),
|
||||||
|
ANYSTR,
|
||||||
|
ANYSTR,
|
||||||
|
realname,
|
||||||
|
],
|
||||||
|
)
|
||||||
# dumb regression test for oragono/oragono#355:
|
# dumb regression test for oragono/oragono#355:
|
||||||
self.assertNotIn(
|
self.assertNotIn(
|
||||||
whois_user.params[3], [nick, username, "~" + username, realname]
|
whois_user.params[3], [nick, username, "~" + username, realname]
|
||||||
)
|
)
|
||||||
self.assertEqual(whois_user.params[5], realname)
|
|
||||||
|
@cases.mark_specifications("RFC2812")
|
||||||
|
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",
|
||||||
|
@ -7,6 +7,7 @@ The WHOSWAS command (`RFC 1459
|
|||||||
TODO: cross-reference Modern
|
TODO: cross-reference Modern
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@ -98,7 +99,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
"Servers MUST reply with either ERR_WASNOSUCHNICK or [...],
|
"Servers MUST reply with either ERR_WASNOSUCHNICK or [...],
|
||||||
both followed with RPL_ENDOFWHOWAS"
|
both followed with RPL_ENDOFWHOWAS"
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/170
|
-- https://modern.ircdocs.horse/#whowas-message
|
||||||
"""
|
"""
|
||||||
self.connectClient("nick1")
|
self.connectClient("nick1")
|
||||||
|
|
||||||
@ -144,6 +145,8 @@ 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:
|
||||||
@ -151,6 +154,9 @@ 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)
|
||||||
@ -201,59 +207,46 @@ class WhowasTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["InspIRCd"],
|
|
||||||
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1967",
|
|
||||||
)
|
|
||||||
def testWhowasMultiple(self):
|
def testWhowasMultiple(self):
|
||||||
"""
|
"""
|
||||||
"The history is searched backward, returning the most recent entry first."
|
"The history is searched backward, returning the most recent entry first."
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/170
|
-- https://modern.ircdocs.horse/#whowas-message
|
||||||
"""
|
"""
|
||||||
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2")
|
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2")
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["InspIRCd"],
|
|
||||||
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
|
|
||||||
)
|
|
||||||
def testWhowasCount1(self):
|
def testWhowasCount1(self):
|
||||||
"""
|
"""
|
||||||
"If there are multiple entries, up to <count> replies will be returned"
|
"If there are multiple entries, up to <count> replies will be returned"
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/170
|
-- https://modern.ircdocs.horse/#whowas-message
|
||||||
"""
|
"""
|
||||||
self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1")
|
self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1")
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["InspIRCd"],
|
|
||||||
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
|
|
||||||
)
|
|
||||||
def testWhowasCount2(self):
|
def testWhowasCount2(self):
|
||||||
"""
|
"""
|
||||||
"If there are multiple entries, up to <count> replies will be returned"
|
"If there are multiple entries, up to <count> replies will be returned"
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/170
|
-- https://modern.ircdocs.horse/#whowas-message
|
||||||
"""
|
"""
|
||||||
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2")
|
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2")
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["InspIRCd"],
|
|
||||||
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
|
|
||||||
)
|
|
||||||
def testWhowasCountNegative(self):
|
def testWhowasCountNegative(self):
|
||||||
"""
|
"""
|
||||||
"If a non-positive number is passed as being <count>, then a full search
|
"If a non-positive number is passed as being <count>, then a full search
|
||||||
is done."
|
is done."
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/170
|
|
||||||
|
"If given, <count> SHOULD be a positive number. Otherwise, a full search
|
||||||
|
"is done.
|
||||||
|
-- https://modern.ircdocs.horse/#whowas-message
|
||||||
"""
|
"""
|
||||||
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 -1")
|
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 -1")
|
||||||
|
|
||||||
@ -261,17 +254,16 @@ class WhowasTestCase(cases.BaseServerTestCase):
|
|||||||
@cases.xfailIfSoftware(
|
@cases.xfailIfSoftware(
|
||||||
["ircu2"], "Fix not released yet: https://github.com/UndernetIRC/ircu2/pull/19"
|
["ircu2"], "Fix not released yet: https://github.com/UndernetIRC/ircu2/pull/19"
|
||||||
)
|
)
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["InspIRCd"],
|
|
||||||
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1967",
|
|
||||||
)
|
|
||||||
def testWhowasCountZero(self):
|
def testWhowasCountZero(self):
|
||||||
"""
|
"""
|
||||||
"If a non-positive number is passed as being <count>, then a full search
|
"If a non-positive number is passed as being <count>, then a full search
|
||||||
is done."
|
is done."
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/170
|
|
||||||
|
"If given, <count> SHOULD be a positive number. Otherwise, a full search
|
||||||
|
"is done.
|
||||||
|
-- https://modern.ircdocs.horse/#whowas-message
|
||||||
"""
|
"""
|
||||||
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 0")
|
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 0")
|
||||||
|
|
||||||
@ -280,7 +272,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
|
|||||||
"""
|
"""
|
||||||
"Wildcards are allowed in the <target> parameter."
|
"Wildcards are allowed in the <target> parameter."
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/170
|
-- https://modern.ircdocs.horse/#whowas-message
|
||||||
"""
|
"""
|
||||||
if self.controller.software_name == "Bahamut":
|
if self.controller.software_name == "Bahamut":
|
||||||
raise runner.OptionalExtensionNotSupported("WHOWAS mask")
|
raise runner.OptionalExtensionNotSupported("WHOWAS mask")
|
||||||
@ -324,7 +316,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
|
|||||||
"""
|
"""
|
||||||
"If the `<nick>` argument is missing, they SHOULD send a single reply, using
|
"If the `<nick>` argument is missing, they SHOULD send a single reply, using
|
||||||
either ERR_NONICKNAMEGIVEN or ERR_NEEDMOREPARAMS"
|
either ERR_NONICKNAMEGIVEN or ERR_NEEDMOREPARAMS"
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/170
|
-- https://modern.ircdocs.horse/#whowas-message
|
||||||
"""
|
"""
|
||||||
# But no one seems to follow this. Most implementations use ERR_NEEDMOREPARAMS
|
# But no one seems to follow this. Most implementations use ERR_NEEDMOREPARAMS
|
||||||
# instead of ERR_NONICKNAMEGIVEN; and I couldn't find any that returns
|
# instead of ERR_NONICKNAMEGIVEN; and I couldn't find any that returns
|
||||||
@ -358,7 +350,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
|
|||||||
"""
|
"""
|
||||||
https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3
|
||||||
https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/170
|
-- https://modern.ircdocs.horse/#whowas-message
|
||||||
|
|
||||||
and:
|
and:
|
||||||
|
|
||||||
@ -371,7 +363,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
"Servers MUST reply with either ERR_WASNOSUCHNICK or [...],
|
"Servers MUST reply with either ERR_WASNOSUCHNICK or [...],
|
||||||
both followed with RPL_ENDOFWHOWAS"
|
both followed with RPL_ENDOFWHOWAS"
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/170
|
-- https://modern.ircdocs.horse/#whowas-message
|
||||||
"""
|
"""
|
||||||
self.connectClient("nick1")
|
self.connectClient("nick1")
|
||||||
|
|
||||||
|
@ -27,16 +27,19 @@ class Specifications(enum.Enum):
|
|||||||
|
|
||||||
@enum.unique
|
@enum.unique
|
||||||
class Capabilities(enum.Enum):
|
class Capabilities(enum.Enum):
|
||||||
|
ACCOUNT_NOTIFY = "account-notify"
|
||||||
ACCOUNT_TAG = "account-tag"
|
ACCOUNT_TAG = "account-tag"
|
||||||
AWAY_NOTIFY = "away-notify"
|
AWAY_NOTIFY = "away-notify"
|
||||||
BATCH = "batch"
|
BATCH = "batch"
|
||||||
ECHO_MESSAGE = "echo-message"
|
ECHO_MESSAGE = "echo-message"
|
||||||
EXTENDED_JOIN = "extended-join"
|
EXTENDED_JOIN = "extended-join"
|
||||||
|
EXTENDED_MONITOR = "extended-monitor"
|
||||||
LABELED_RESPONSE = "labeled-response"
|
LABELED_RESPONSE = "labeled-response"
|
||||||
MESSAGE_TAGS = "message-tags"
|
MESSAGE_TAGS = "message-tags"
|
||||||
MULTILINE = "draft/multiline"
|
MULTILINE = "draft/multiline"
|
||||||
MULTI_PREFIX = "multi-prefix"
|
MULTI_PREFIX = "multi-prefix"
|
||||||
SERVER_TIME = "server-time"
|
SERVER_TIME = "server-time"
|
||||||
|
SETNAME = "setname"
|
||||||
STS = "sts"
|
STS = "sts"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -56,6 +59,7 @@ class IsupportTokens(enum.Enum):
|
|||||||
MONITOR = "MONITOR"
|
MONITOR = "MONITOR"
|
||||||
STATUSMSG = "STATUSMSG"
|
STATUSMSG = "STATUSMSG"
|
||||||
TARGMAX = "TARGMAX"
|
TARGMAX = "TARGMAX"
|
||||||
|
UTF8ONLY = "UTF8ONLY"
|
||||||
WHOX = "WHOX"
|
WHOX = "WHOX"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -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@v2",
|
"uses": "actions/checkout@v4",
|
||||||
"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@v2",
|
"uses": "actions/cache@v4",
|
||||||
"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 }}-"
|
||||||
@ -116,18 +116,18 @@ def get_build_job(*, software_config, software_id, version_flavor):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"runs-on": "ubuntu-latest",
|
"runs-on": "ubuntu-22.04",
|
||||||
"steps": [
|
"steps": [
|
||||||
{
|
{
|
||||||
"name": "Create directories",
|
"name": "Create directories",
|
||||||
"run": "cd ~/; mkdir -p .local/ go/",
|
"run": "cd ~/; mkdir -p .local/ go/",
|
||||||
},
|
},
|
||||||
*cache,
|
*cache,
|
||||||
{"uses": "actions/checkout@v2"},
|
{"uses": "actions/checkout@v4"},
|
||||||
{
|
{
|
||||||
"name": "Set up Python 3.7",
|
"name": "Set up Python 3.11",
|
||||||
"uses": "actions/setup-python@v2",
|
"uses": "actions/setup-python@v5",
|
||||||
"with": {"python-version": 3.7},
|
"with": {"python-version": 3.11},
|
||||||
},
|
},
|
||||||
*install_steps,
|
*install_steps,
|
||||||
*upload_steps(software_id),
|
*upload_steps(software_id),
|
||||||
@ -146,11 +146,12 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
|
|||||||
for software_id in test_config.get("software", []):
|
for software_id in test_config.get("software", []):
|
||||||
software_config = config["software"][software_id]
|
software_config = config["software"][software_id]
|
||||||
|
|
||||||
env += test_config.get("env", {}).get(version_flavor.value, "") + " "
|
env += software_config.get("env", "") + " "
|
||||||
if "prefix" in software_config:
|
if "prefix" in software_config:
|
||||||
env += (
|
env += (
|
||||||
f"PATH={software_config['prefix']}/sbin"
|
f"PATH={software_config['prefix']}/sbin"
|
||||||
f":{software_config['prefix']}/bin"
|
f":{software_config['prefix']}/bin"
|
||||||
|
f":{software_config['prefix']}"
|
||||||
f":$PATH "
|
f":$PATH "
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -159,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@v2",
|
"uses": "actions/download-artifact@v4",
|
||||||
"with": {"name": f"installed-{software_id}", "path": "~"},
|
"with": {"name": f"installed-{software_id}", "path": "~"},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -191,14 +192,14 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
|
|||||||
unpack = []
|
unpack = []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"runs-on": "ubuntu-latest",
|
"runs-on": "ubuntu-22.04",
|
||||||
"needs": needs,
|
"needs": needs,
|
||||||
"steps": [
|
"steps": [
|
||||||
{"uses": "actions/checkout@v2"},
|
{"uses": "actions/checkout@v4"},
|
||||||
{
|
{
|
||||||
"name": "Set up Python 3.7",
|
"name": "Set up Python 3.11",
|
||||||
"uses": "actions/setup-python@v2",
|
"uses": "actions/setup-python@v5",
|
||||||
"with": {"python-version": 3.7},
|
"with": {"python-version": 3.11},
|
||||||
},
|
},
|
||||||
*downloads,
|
*downloads,
|
||||||
*unpack,
|
*unpack,
|
||||||
@ -211,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 -r requirements.txt",
|
"pip install pytest pytest-xdist pytest-timeout -r requirements.txt",
|
||||||
*(
|
*(
|
||||||
software_config["extra_deps"]
|
software_config["extra_deps"]
|
||||||
if "extra_deps" in software_config
|
if "extra_deps" in software_config
|
||||||
@ -222,8 +223,11 @@ 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' "
|
f"PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' "
|
||||||
f"PATH=$HOME/.local/bin:$PATH "
|
f"PATH=$HOME/.local/bin:$PATH "
|
||||||
f"{env}make {test_id}"
|
f"{env}make {test_id}"
|
||||||
),
|
),
|
||||||
@ -231,7 +235,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@v2",
|
"uses": "actions/upload-artifact@v4",
|
||||||
"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",
|
||||||
@ -250,7 +254,7 @@ def upload_steps(software_id):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Upload build artefacts",
|
"name": "Upload build artefacts",
|
||||||
"uses": "actions/upload-artifact@v2",
|
"uses": "actions/upload-artifact@v4",
|
||||||
"with": {
|
"with": {
|
||||||
"name": f"installed-{software_id}",
|
"name": f"installed-{software_id}",
|
||||||
"path": "~/artefacts-*.tar.gz",
|
"path": "~/artefacts-*.tar.gz",
|
||||||
@ -263,7 +267,6 @@ def upload_steps(software_id):
|
|||||||
|
|
||||||
|
|
||||||
def generate_workflow(config: dict, version_flavor: VersionFlavor):
|
def generate_workflow(config: dict, version_flavor: VersionFlavor):
|
||||||
|
|
||||||
on: dict
|
on: dict
|
||||||
if version_flavor == VersionFlavor.STABLE:
|
if version_flavor == VersionFlavor.STABLE:
|
||||||
on = {"push": None, "pull_request": None}
|
on = {"push": None, "pull_request": None}
|
||||||
@ -307,15 +310,15 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor):
|
|||||||
jobs["publish-test-results"] = {
|
jobs["publish-test-results"] = {
|
||||||
"name": "Publish Dashboard",
|
"name": "Publish Dashboard",
|
||||||
"needs": sorted({f"test-{test_id}" for test_id in config["tests"]} & set(jobs)),
|
"needs": sorted({f"test-{test_id}" for test_id in config["tests"]} & set(jobs)),
|
||||||
"runs-on": "ubuntu-latest",
|
"runs-on": "ubuntu-22.04",
|
||||||
# the build-and-test job might be skipped, we don't need to run
|
# the build-and-test job might be skipped, we don't need to run
|
||||||
# this job then
|
# this job then
|
||||||
"if": "success() || failure()",
|
"if": "success() || failure()",
|
||||||
"steps": [
|
"steps": [
|
||||||
{"uses": "actions/checkout@v2"},
|
{"uses": "actions/checkout@v4"},
|
||||||
{
|
{
|
||||||
"name": "Download Artifacts",
|
"name": "Download Artifacts",
|
||||||
"uses": "actions/download-artifact@v2",
|
"uses": "actions/download-artifact@v4",
|
||||||
"with": {"path": "artifacts"},
|
"with": {"path": "artifacts"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
5
mypy.ini
5
mypy.ini
@ -1,5 +1,5 @@
|
|||||||
[mypy]
|
[mypy]
|
||||||
python_version = 3.7
|
python_version = 3.9
|
||||||
warn_return_any = True
|
warn_return_any = True
|
||||||
warn_unused_configs = True
|
warn_unused_configs = True
|
||||||
|
|
||||||
@ -12,6 +12,9 @@ disallow_untyped_defs = False
|
|||||||
[mypy-irctest.client_tests.*]
|
[mypy-irctest.client_tests.*]
|
||||||
disallow_untyped_defs = False
|
disallow_untyped_defs = False
|
||||||
|
|
||||||
|
[mypy-irctest.self_tests.*]
|
||||||
|
disallow_untyped_defs = False
|
||||||
|
|
||||||
[mypy-defusedxml.*]
|
[mypy-defusedxml.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
@ -8,13 +8,13 @@ index 317b00e..adfcfcf 100644
|
|||||||
dots = 1;
|
dots = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
- if (!dots)
|
- if (!dots)
|
||||||
- {
|
- {
|
||||||
- sendto_realops("Invalid hostname for %s, dumping user %s",
|
- sendto_realops("Invalid hostname for %s, dumping user %s",
|
||||||
- sptr->hostip, sptr->name);
|
- sptr->hostip, sptr->name);
|
||||||
- return exit_client(cptr, sptr, &me, "Invalid hostname");
|
- return exit_client(cptr, sptr, &me, "Invalid hostname");
|
||||||
- }
|
- }
|
||||||
-
|
-
|
||||||
if (bad_dns)
|
if (bad_dns)
|
||||||
{
|
{
|
||||||
sendto_one(sptr, ":%s NOTICE %s :*** Notice -- You have a bad "
|
sendto_one(sptr, ":%s NOTICE %s :*** Notice -- You have a bad "
|
||||||
|
342
patches/bahamut_ubuntu22.patch
Normal file
342
patches/bahamut_ubuntu22.patch
Normal file
@ -0,0 +1,342 @@
|
|||||||
|
From 42b67ff7218877934abed2a738e164c0dea171b0 Mon Sep 17 00:00:00 2001
|
||||||
|
From: "Ned T. Crigler" <RuneB@dal.net>
|
||||||
|
Date: Sun, 26 Feb 2023 17:42:29 -0800
|
||||||
|
Subject: [PATCH 1/2] Fix compilation on Ubuntu 22.04
|
||||||
|
|
||||||
|
Starting with glibc 2.34 "The symbols __dn_comp, __dn_expand,
|
||||||
|
__dn_skipname, __res_dnok, __res_hnok, __res_mailok, __res_mkquery,
|
||||||
|
__res_nmkquery, __res_nquery, __res_nquerydomain, __res_nsearch,
|
||||||
|
__res_nsend, __res_ownok, __res_query, __res_querydomain, __res_search,
|
||||||
|
__res_send formerly in libresolv have been renamed and no longer have a
|
||||||
|
__ prefix. They are now available in libc."
|
||||||
|
https://sourceware.org/pipermail/libc-alpha/2021-August/129718.html
|
||||||
|
|
||||||
|
The hex_to_string array in include/dh.h also conflicts with OpenSSL,
|
||||||
|
which OpenSSL 3.0 now complains about.
|
||||||
|
---
|
||||||
|
configure.in | 4 ++--
|
||||||
|
include/dh.h | 2 +-
|
||||||
|
include/resolv.h | 6 +++++-
|
||||||
|
src/dh.c | 2 +-
|
||||||
|
4 files changed, 9 insertions(+), 5 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/configure.in b/configure.in
|
||||||
|
index e76dee88..11720419 100644
|
||||||
|
--- a/configure.in
|
||||||
|
+++ b/configure.in
|
||||||
|
@@ -374,8 +374,7 @@ AC_C_INLINE
|
||||||
|
dnl Checks for libraries.
|
||||||
|
dnl Replace `main' with a function in -lnsl:
|
||||||
|
AC_CHECK_LIB(nsl, gethostbyname)
|
||||||
|
-AC_CHECK_FUNC(res_mkquery,, AC_CHECK_LIB(resolv, res_mkquery))
|
||||||
|
-AC_CHECK_FUNC(__res_mkquery,, AC_CHECK_LIB(resolv, __res_mkquery))
|
||||||
|
+AC_SEARCH_LIBS([res_mkquery],[resolv],,AC_SEARCH_LIBS([__res_mkquery],[resolv]))
|
||||||
|
AC_CHECK_LIB(socket, socket, zlib)
|
||||||
|
AC_CHECK_FUNC(crypt,, AC_CHECK_LIB(descrypt, crypt,,AC_CHECK_LIB(crypt, crypt,,)))
|
||||||
|
|
||||||
|
@@ -406,6 +405,7 @@ AC_CHECK_FUNCS([strcasecmp strchr strdup strerror strncasecmp strrchr strtol])
|
||||||
|
AC_CHECK_FUNCS([strtoul index strerror strtoken strtok inet_addr inet_netof])
|
||||||
|
AC_CHECK_FUNCS([inet_aton gettimeofday lrand48 sigaction bzero bcmp bcopy])
|
||||||
|
AC_CHECK_FUNCS([dn_skipname __dn_skipname getrusage times break])
|
||||||
|
+AC_CHECK_FUNCS([res_init __res_init res_mkquery __res_mkquery dn_expand __dn_expand])
|
||||||
|
|
||||||
|
dnl check for various OSes
|
||||||
|
|
||||||
|
diff --git a/include/dh.h b/include/dh.h
|
||||||
|
index 1ca6996a..1817ce1e 100644
|
||||||
|
--- a/include/dh.h
|
||||||
|
+++ b/include/dh.h
|
||||||
|
@@ -45,7 +45,7 @@ struct session_info
|
||||||
|
static BIGNUM *ircd_prime;
|
||||||
|
static BIGNUM *ircd_generator;
|
||||||
|
|
||||||
|
-static char *hex_to_string[256] =
|
||||||
|
+static char *dh_hex_to_string[256] =
|
||||||
|
{
|
||||||
|
"00", "01", "02", "03", "04", "05", "06", "07",
|
||||||
|
"08", "09", "0a", "0b", "0c", "0d", "0e", "0f",
|
||||||
|
diff --git a/include/resolv.h b/include/resolv.h
|
||||||
|
index b5a8aaa1..5b042d43 100644
|
||||||
|
--- a/include/resolv.h
|
||||||
|
+++ b/include/resolv.h
|
||||||
|
@@ -106,9 +106,13 @@ extern struct state _res;
|
||||||
|
|
||||||
|
extern char *p_cdname(), *p_rr(), *p_type(), *p_class(), *p_time();
|
||||||
|
|
||||||
|
-#if ((__GNU_LIBRARY__ == 6) && (__GLIBC__ >=2) && (__GLIBC_MINOR__ >= 2))
|
||||||
|
+#if !defined(HAVE_RES_INIT) && defined(HAVE___RES_INIT)
|
||||||
|
#define res_init __res_init
|
||||||
|
+#endif
|
||||||
|
+#if !defined(HAVE_RES_MKQUERY) && defined(HAVE___RES_MKQUERY)
|
||||||
|
#define res_mkquery __res_mkquery
|
||||||
|
+#endif
|
||||||
|
+#if !defined(HAVE_DN_EXPAND) && defined(HAVE___DN_EXPAND)
|
||||||
|
#define dn_expand __dn_expand
|
||||||
|
#endif
|
||||||
|
|
||||||
|
diff --git a/src/dh.c b/src/dh.c
|
||||||
|
index cb065a4f..4b5da282 100644
|
||||||
|
--- a/src/dh.c
|
||||||
|
+++ b/src/dh.c
|
||||||
|
@@ -223,7 +223,7 @@ static void create_prime()
|
||||||
|
|
||||||
|
for(i = 0; i < PRIME_BYTES; i++)
|
||||||
|
{
|
||||||
|
- char *x = hex_to_string[dh_prime_1024[i]];
|
||||||
|
+ char *x = dh_hex_to_string[dh_prime_1024[i]];
|
||||||
|
while(*x)
|
||||||
|
buf[bufpos++] = *x++;
|
||||||
|
}
|
||||||
|
|
||||||
|
From 135ebbea4c30e23228d00af762fa7da7ca5016bd Mon Sep 17 00:00:00 2001
|
||||||
|
From: "Ned T. Crigler" <RuneB@dal.net>
|
||||||
|
Date: Mon, 22 May 2023 15:31:54 -0700
|
||||||
|
Subject: [PATCH 2/2] Update the dh code to work with OpenSSL 3.0
|
||||||
|
|
||||||
|
---
|
||||||
|
include/dh.h | 8 ++++
|
||||||
|
src/dh.c | 120 ++++++++++++++++++++++++++++++++++++++++++++++++---
|
||||||
|
2 files changed, 123 insertions(+), 5 deletions(-)
|
||||||
|
|
||||||
|
diff --git a/include/dh.h b/include/dh.h
|
||||||
|
index 1817ce1e..705e6dee 100644
|
||||||
|
--- a/include/dh.h
|
||||||
|
+++ b/include/dh.h
|
||||||
|
@@ -22,7 +22,11 @@ extern void rc4_destroystate(void *a);
|
||||||
|
|
||||||
|
struct session_info
|
||||||
|
{
|
||||||
|
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
|
||||||
|
DH *dh;
|
||||||
|
+#else
|
||||||
|
+ EVP_PKEY *dh;
|
||||||
|
+#endif
|
||||||
|
unsigned char *session_shared;
|
||||||
|
size_t session_shared_length;
|
||||||
|
};
|
||||||
|
@@ -45,6 +49,10 @@ struct session_info
|
||||||
|
static BIGNUM *ircd_prime;
|
||||||
|
static BIGNUM *ircd_generator;
|
||||||
|
|
||||||
|
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||||
|
+static EVP_PKEY *ircd_prime_ossl3;
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
static char *dh_hex_to_string[256] =
|
||||||
|
{
|
||||||
|
"00", "01", "02", "03", "04", "05", "06", "07",
|
||||||
|
diff --git a/src/dh.c b/src/dh.c
|
||||||
|
index 4b5da282..f74d2d76 100644
|
||||||
|
--- a/src/dh.c
|
||||||
|
+++ b/src/dh.c
|
||||||
|
@@ -36,6 +36,11 @@
|
||||||
|
#include <openssl/dh.h>
|
||||||
|
#include "libcrypto-compat.h"
|
||||||
|
|
||||||
|
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||||
|
+#include <openssl/core_names.h>
|
||||||
|
+#include <openssl/param_build.h>
|
||||||
|
+#endif
|
||||||
|
+
|
||||||
|
#include "memcount.h"
|
||||||
|
|
||||||
|
#define DH_HEADER
|
||||||
|
@@ -215,7 +220,7 @@ static int init_random()
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
-static void create_prime()
|
||||||
|
+static int create_prime()
|
||||||
|
{
|
||||||
|
char buf[PRIME_BYTES_HEX];
|
||||||
|
int i;
|
||||||
|
@@ -233,6 +238,34 @@ static void create_prime()
|
||||||
|
BN_hex2bn(&ircd_prime, buf);
|
||||||
|
ircd_generator = BN_new();
|
||||||
|
BN_set_word(ircd_generator, dh_gen_1024);
|
||||||
|
+
|
||||||
|
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
|
||||||
|
+ OSSL_PARAM_BLD *paramBuild = NULL;
|
||||||
|
+ OSSL_PARAM *param = NULL;
|
||||||
|
+ EVP_PKEY_CTX *primeCtx = NULL;
|
||||||
|
+
|
||||||
|
+ if(!(paramBuild = OSSL_PARAM_BLD_new()) ||
|
||||||
|
+ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_P, ircd_prime) ||
|
||||||
|
+ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_G, ircd_generator) ||
|
||||||
|
+ !(param = OSSL_PARAM_BLD_to_param(paramBuild)) ||
|
||||||
|
+ !(primeCtx = EVP_PKEY_CTX_new_from_name(NULL, "DHX", NULL)) ||
|
||||||
|
+ EVP_PKEY_fromdata_init(primeCtx) <= 0 ||
|
||||||
|
+ EVP_PKEY_fromdata(primeCtx, &ircd_prime_ossl3,
|
||||||
|
+ EVP_PKEY_KEY_PARAMETERS, param) <= 0 ||
|
||||||
|
+ 1)
|
||||||
|
+ {
|
||||||
|
+ if(primeCtx)
|
||||||
|
+ EVP_PKEY_CTX_free(primeCtx);
|
||||||
|
+ if(param)
|
||||||
|
+ OSSL_PARAM_free(param);
|
||||||
|
+ if(paramBuild)
|
||||||
|
+ OSSL_PARAM_BLD_free(paramBuild);
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ if(!ircd_prime_ossl3)
|
||||||
|
+ return -1;
|
||||||
|
+#endif
|
||||||
|
+ return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dh_init()
|
||||||
|
@@ -241,8 +274,7 @@ int dh_init()
|
||||||
|
ERR_load_crypto_strings();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
- create_prime();
|
||||||
|
- if(init_random() == -1)
|
||||||
|
+ if(create_prime() == -1 || init_random() == -1)
|
||||||
|
return -1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
@@ -250,7 +282,7 @@ int dh_init()
|
||||||
|
int dh_generate_shared(void *session, char *public_key)
|
||||||
|
{
|
||||||
|
BIGNUM *tmp;
|
||||||
|
- int len;
|
||||||
|
+ size_t len;
|
||||||
|
struct session_info *si = (struct session_info *) session;
|
||||||
|
|
||||||
|
if(verify_is_hex(public_key) == 0 || !si || si->session_shared)
|
||||||
|
@@ -261,13 +293,55 @@ int dh_generate_shared(void *session, char *public_key)
|
||||||
|
if(!tmp)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
|
||||||
|
si->session_shared_length = DH_size(si->dh);
|
||||||
|
si->session_shared = (unsigned char *) malloc(DH_size(si->dh));
|
||||||
|
len = DH_compute_key(si->session_shared, tmp, si->dh);
|
||||||
|
+#else
|
||||||
|
+ OSSL_PARAM_BLD *paramBuild = NULL;
|
||||||
|
+ OSSL_PARAM *param = NULL;
|
||||||
|
+ EVP_PKEY_CTX *peerPubKeyCtx = NULL;
|
||||||
|
+ EVP_PKEY *peerPubKey = NULL;
|
||||||
|
+ EVP_PKEY_CTX *deriveCtx = NULL;
|
||||||
|
+
|
||||||
|
+ len = -1;
|
||||||
|
+ if(!(paramBuild = OSSL_PARAM_BLD_new()) ||
|
||||||
|
+ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_P, ircd_prime) ||
|
||||||
|
+ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_G, ircd_generator) ||
|
||||||
|
+ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_PUB_KEY, tmp) ||
|
||||||
|
+ !(param = OSSL_PARAM_BLD_to_param(paramBuild)) ||
|
||||||
|
+ !(peerPubKeyCtx = EVP_PKEY_CTX_new_from_name(NULL, "DHX", NULL)) ||
|
||||||
|
+ EVP_PKEY_fromdata_init(peerPubKeyCtx) <= 0 ||
|
||||||
|
+ EVP_PKEY_fromdata(peerPubKeyCtx, &peerPubKey,
|
||||||
|
+ EVP_PKEY_PUBLIC_KEY, param) <= 0 ||
|
||||||
|
+ !(deriveCtx = EVP_PKEY_CTX_new(si->dh, NULL)) ||
|
||||||
|
+ EVP_PKEY_derive_init(deriveCtx) <= 0 ||
|
||||||
|
+ EVP_PKEY_derive_set_peer(deriveCtx, peerPubKey) <= 0 ||
|
||||||
|
+ EVP_PKEY_derive(deriveCtx, NULL, &len) <= 0 ||
|
||||||
|
+ !(si->session_shared = malloc(len)) ||
|
||||||
|
+ EVP_PKEY_derive(deriveCtx, si->session_shared, &len) <= 0 ||
|
||||||
|
+ 1)
|
||||||
|
+ {
|
||||||
|
+ if(deriveCtx)
|
||||||
|
+ EVP_PKEY_CTX_free(deriveCtx);
|
||||||
|
+ if(peerPubKey)
|
||||||
|
+ EVP_PKEY_free(peerPubKey);
|
||||||
|
+ if(peerPubKeyCtx)
|
||||||
|
+ EVP_PKEY_CTX_free(peerPubKeyCtx);
|
||||||
|
+ if(param)
|
||||||
|
+ OSSL_PARAM_free(param);
|
||||||
|
+ if(paramBuild)
|
||||||
|
+ OSSL_PARAM_BLD_free(paramBuild);
|
||||||
|
+ }
|
||||||
|
+#endif
|
||||||
|
BN_free(tmp);
|
||||||
|
|
||||||
|
- if(len < 0)
|
||||||
|
+ if(len == -1 || !si->session_shared)
|
||||||
|
+ {
|
||||||
|
+ if(si->session_shared)
|
||||||
|
+ free(si->session_shared);
|
||||||
|
return 0;
|
||||||
|
+ }
|
||||||
|
|
||||||
|
si->session_shared_length = len;
|
||||||
|
|
||||||
|
@@ -284,6 +358,7 @@ void *dh_start_session()
|
||||||
|
|
||||||
|
memset(si, 0, sizeof(struct session_info));
|
||||||
|
|
||||||
|
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
|
||||||
|
si->dh = DH_new();
|
||||||
|
if(si->dh == NULL)
|
||||||
|
return NULL;
|
||||||
|
@@ -304,7 +379,23 @@ void *dh_start_session()
|
||||||
|
MyFree(si);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
+#else
|
||||||
|
+ EVP_PKEY_CTX *keyGenCtx = NULL;
|
||||||
|
|
||||||
|
+ if(!(keyGenCtx = EVP_PKEY_CTX_new_from_pkey(NULL, ircd_prime_ossl3, NULL)) ||
|
||||||
|
+ EVP_PKEY_keygen_init(keyGenCtx) <= 0 ||
|
||||||
|
+ EVP_PKEY_generate(keyGenCtx, &si->dh) <= 0 ||
|
||||||
|
+ 1)
|
||||||
|
+ {
|
||||||
|
+ if(keyGenCtx)
|
||||||
|
+ EVP_PKEY_CTX_free(keyGenCtx);
|
||||||
|
+ }
|
||||||
|
+ if(!si->dh)
|
||||||
|
+ {
|
||||||
|
+ MyFree(si);
|
||||||
|
+ return NULL;
|
||||||
|
+ }
|
||||||
|
+#endif
|
||||||
|
return (void *) si;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -312,6 +403,7 @@ void dh_end_session(void *session)
|
||||||
|
{
|
||||||
|
struct session_info *si = (struct session_info *) session;
|
||||||
|
|
||||||
|
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
|
||||||
|
if(si->dh)
|
||||||
|
{
|
||||||
|
DH_free(si->dh);
|
||||||
|
@@ -324,6 +416,13 @@ void dh_end_session(void *session)
|
||||||
|
free(si->session_shared);
|
||||||
|
si->session_shared = NULL;
|
||||||
|
}
|
||||||
|
+#else
|
||||||
|
+ if(si->dh)
|
||||||
|
+ {
|
||||||
|
+ EVP_PKEY_free(si->dh);
|
||||||
|
+ si->dh = NULL;
|
||||||
|
+ }
|
||||||
|
+#endif
|
||||||
|
|
||||||
|
MyFree(si);
|
||||||
|
}
|
||||||
|
@@ -333,6 +432,7 @@ char *dh_get_s_public(char *buf, size_t maxlen, void *session)
|
||||||
|
struct session_info *si = (struct session_info *) session;
|
||||||
|
char *tmp;
|
||||||
|
|
||||||
|
+#if OPENSSL_VERSION_NUMBER < 0x30000000L
|
||||||
|
if(!si || !si->dh)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
@@ -343,6 +443,16 @@ char *dh_get_s_public(char *buf, size_t maxlen, void *session)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
tmp = BN_bn2hex(pub_key);
|
||||||
|
+#else
|
||||||
|
+ BIGNUM *pub_key = NULL;
|
||||||
|
+
|
||||||
|
+ if(!si || !si->dh)
|
||||||
|
+ return NULL;
|
||||||
|
+ if(!EVP_PKEY_get_bn_param(si->dh, OSSL_PKEY_PARAM_PUB_KEY, &pub_key))
|
||||||
|
+ return NULL;
|
||||||
|
+ tmp = BN_bn2hex(pub_key);
|
||||||
|
+ BN_free(pub_key);
|
||||||
|
+#endif
|
||||||
|
if(!tmp)
|
||||||
|
return NULL;
|
||||||
|
|
23
patches/charybdis_ubuntu22.patch
Normal file
23
patches/charybdis_ubuntu22.patch
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
From fa5d445e5e2af735378a1219d2a200ee8aef6561 Mon Sep 17 00:00:00 2001
|
||||||
|
From: Sadie Powell <sadie@witchery.services>
|
||||||
|
Date: Sun, 25 Jun 2023 21:50:42 +0100
|
||||||
|
Subject: [PATCH] Fix Charybdis on Ubuntu 22.04.
|
||||||
|
|
||||||
|
---
|
||||||
|
librb/include/rb_lib.h | 2 ++
|
||||||
|
1 file changed, 2 insertions(+)
|
||||||
|
|
||||||
|
diff --git a/librb/include/rb_lib.h b/librb/include/rb_lib.h
|
||||||
|
index c02dff68..0dd9c378 100644
|
||||||
|
--- a/librb/include/rb_lib.h
|
||||||
|
+++ b/librb/include/rb_lib.h
|
||||||
|
@@ -258,4 +258,6 @@ pid_t rb_getpid(void);
|
||||||
|
#include <rb_rawbuf.h>
|
||||||
|
#include <rb_patricia.h>
|
||||||
|
|
||||||
|
+#include <time.h>
|
||||||
|
+
|
||||||
|
#endif
|
||||||
|
--
|
||||||
|
2.34.1
|
||||||
|
|
@ -1,25 +0,0 @@
|
|||||||
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 = ['py37']
|
target-version = ['py38']
|
||||||
exclude = 'irctest/scram/*'
|
exclude = 'irctest/scram/*'
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
|
@ -18,16 +18,19 @@ markers =
|
|||||||
private_chathistory
|
private_chathistory
|
||||||
|
|
||||||
# capabilities
|
# capabilities
|
||||||
|
account-notify
|
||||||
account-tag
|
account-tag
|
||||||
away-notify
|
away-notify
|
||||||
batch
|
batch
|
||||||
echo-message
|
echo-message
|
||||||
extended-join
|
extended-join
|
||||||
|
extended-monitor
|
||||||
labeled-response
|
labeled-response
|
||||||
message-tags
|
message-tags
|
||||||
draft/multiline
|
draft/multiline
|
||||||
multi-prefix
|
multi-prefix
|
||||||
server-time
|
server-time
|
||||||
|
setname
|
||||||
sts
|
sts
|
||||||
|
|
||||||
# isupport tokens
|
# isupport tokens
|
||||||
@ -38,6 +41,7 @@ markers =
|
|||||||
PREFIX
|
PREFIX
|
||||||
STATUSMSG
|
STATUSMSG
|
||||||
TARGMAX
|
TARGMAX
|
||||||
|
UTF8ONLY
|
||||||
WHOX
|
WHOX
|
||||||
|
|
||||||
python_classes = *TestCase Test*
|
python_classes = *TestCase Test*
|
||||||
|
@ -42,7 +42,7 @@ def partial_compaction(d):
|
|||||||
# tests separate
|
# tests separate
|
||||||
compacted_d = {}
|
compacted_d = {}
|
||||||
successes = []
|
successes = []
|
||||||
for (k, v) in d.items():
|
for k, v in d.items():
|
||||||
if isinstance(v, CompactedResult) and v.success and v.nb_skipped == 0:
|
if isinstance(v, CompactedResult) and v.success and v.nb_skipped == 0:
|
||||||
successes.append((k, v))
|
successes.append((k, v))
|
||||||
else:
|
else:
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
pytest
|
||||||
|
|
||||||
# The following dependencies are actually optional:
|
# The following dependencies are actually optional:
|
||||||
ecdsa
|
ecdsa
|
||||||
pytest
|
filelock
|
||||||
|
118
workflows.yml
118
workflows.yml
@ -18,6 +18,7 @@ software:
|
|||||||
separate_build_job: true
|
separate_build_job: true
|
||||||
build_script: |
|
build_script: |
|
||||||
cd $GITHUB_WORKSPACE/charybdis/
|
cd $GITHUB_WORKSPACE/charybdis/
|
||||||
|
patch -p1 < $GITHUB_WORKSPACE/patches/charybdis_ubuntu22.patch
|
||||||
./autogen.sh
|
./autogen.sh
|
||||||
./configure --prefix=$HOME/.local/
|
./configure --prefix=$HOME/.local/
|
||||||
make -j 4
|
make -j 4
|
||||||
@ -32,6 +33,9 @@ software:
|
|||||||
devel: "8.2.x"
|
devel: "8.2.x"
|
||||||
devel_release: null
|
devel_release: null
|
||||||
path: ircd-hybrid
|
path: ircd-hybrid
|
||||||
|
pre_deps:
|
||||||
|
- name: "Install system dependencies"
|
||||||
|
run: "sudo apt-get install atheme-services faketime libjansson-dev"
|
||||||
separate_build_job: true
|
separate_build_job: true
|
||||||
build_script: |
|
build_script: |
|
||||||
cd $GITHUB_WORKSPACE/ircd-hybrid/
|
cd $GITHUB_WORKSPACE/ircd-hybrid/
|
||||||
@ -96,7 +100,7 @@ software:
|
|||||||
name: Bahamut
|
name: Bahamut
|
||||||
repository: DALnet/Bahamut
|
repository: DALnet/Bahamut
|
||||||
refs:
|
refs:
|
||||||
stable: "v2.2.1"
|
stable: "v2.2.4"
|
||||||
release: null
|
release: null
|
||||||
devel: "master"
|
devel: "master"
|
||||||
devel_release: null
|
devel_release: null
|
||||||
@ -106,6 +110,10 @@ software:
|
|||||||
cd $GITHUB_WORKSPACE/Bahamut/
|
cd $GITHUB_WORKSPACE/Bahamut/
|
||||||
patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch
|
patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch
|
||||||
patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch
|
patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch
|
||||||
|
|
||||||
|
# <= v2.2.2
|
||||||
|
patch -p1 < $GITHUB_WORKSPACE/patches/bahamut_ubuntu22.patch || true
|
||||||
|
|
||||||
echo "#undef THROTTLE_ENABLE" >> include/config.h
|
echo "#undef THROTTLE_ENABLE" >> include/config.h
|
||||||
libtoolize --force
|
libtoolize --force
|
||||||
aclocal
|
aclocal
|
||||||
@ -129,9 +137,9 @@ software:
|
|||||||
path: ergo
|
path: ergo
|
||||||
prefix: ~/go
|
prefix: ~/go
|
||||||
pre_deps:
|
pre_deps:
|
||||||
- uses: actions/setup-go@v2
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: '^1.19.0'
|
go-version: '^1.24.0'
|
||||||
- run: go version
|
- run: go version
|
||||||
separate_build_job: false
|
separate_build_job: false
|
||||||
build_script: |
|
build_script: |
|
||||||
@ -143,7 +151,7 @@ software:
|
|||||||
name: InspIRCd
|
name: InspIRCd
|
||||||
repository: inspircd/inspircd
|
repository: inspircd/inspircd
|
||||||
refs: &inspircd_refs
|
refs: &inspircd_refs
|
||||||
stable: v3.12.0
|
stable: v3.17.1
|
||||||
release: null
|
release: null
|
||||||
devel: master
|
devel: master
|
||||||
devel_release: insp3
|
devel_release: insp3
|
||||||
@ -153,9 +161,8 @@ 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/
|
||||||
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch
|
|
||||||
./configure --prefix=$HOME/.local/inspircd --development
|
./configure --prefix=$HOME/.local/inspircd --development
|
||||||
make -j 4
|
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
|
||||||
make install
|
make install
|
||||||
irc2:
|
irc2:
|
||||||
name: irc2
|
name: irc2
|
||||||
@ -226,7 +233,7 @@ software:
|
|||||||
name: ngircd
|
name: ngircd
|
||||||
repository: ngircd/ngircd
|
repository: ngircd/ngircd
|
||||||
refs:
|
refs:
|
||||||
stable: rel-26.1
|
stable: acf8409c60ccc96beed0a1f990c4f9374823c0ce # three months ahead of v27
|
||||||
release: null
|
release: null
|
||||||
devel: master
|
devel: master
|
||||||
devel_release: null
|
devel_release: null
|
||||||
@ -241,6 +248,34 @@ software:
|
|||||||
make -j 4
|
make -j 4
|
||||||
make install
|
make install
|
||||||
|
|
||||||
|
sable:
|
||||||
|
name: Sable
|
||||||
|
repository: Libera-Chat/sable
|
||||||
|
refs:
|
||||||
|
stable: baed3ef9ac4550dc36a45b758436769e82e8ec58
|
||||||
|
release: null
|
||||||
|
devel: master
|
||||||
|
devel_release: null
|
||||||
|
path: sable
|
||||||
|
prefix: "$GITHUB_WORKSPACE/sable/target/debug"
|
||||||
|
pre_deps:
|
||||||
|
- name: Install rust toolchain
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
- name: Enable Cargo cache
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
with:
|
||||||
|
workspaces: "sable -> target"
|
||||||
|
cache-on-failure: true
|
||||||
|
- run: rustc --version
|
||||||
|
separate_build_job: false
|
||||||
|
build_script: |
|
||||||
|
cd $GITHUB_WORKSPACE/sable/
|
||||||
|
cargo build
|
||||||
|
|
||||||
snircd:
|
snircd:
|
||||||
name: snircd
|
name: snircd
|
||||||
repository: quakenet/snircd
|
repository: quakenet/snircd
|
||||||
@ -268,8 +303,8 @@ software:
|
|||||||
name: UnrealIRCd 6
|
name: UnrealIRCd 6
|
||||||
repository: unrealircd/unrealircd
|
repository: unrealircd/unrealircd
|
||||||
refs:
|
refs:
|
||||||
stable: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3
|
stable: a68625454078641ce984eeb197f7e02b1857ab6c # 6.1.7.1
|
||||||
release: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3
|
release: a68625454078641ce984eeb197f7e02b1857ab6c # 6.1.7.1
|
||||||
devel: unreal60_dev
|
devel: unreal60_dev
|
||||||
devel_release: null
|
devel_release: null
|
||||||
path: unrealircd
|
path: unrealircd
|
||||||
@ -304,23 +339,42 @@ software:
|
|||||||
|
|
||||||
#############################
|
#############################
|
||||||
# Services:
|
# Services:
|
||||||
|
|
||||||
anope:
|
anope:
|
||||||
name: Anope
|
name: Anope
|
||||||
repository: anope/anope
|
repository: anope/anope
|
||||||
separate_build_job: true
|
separate_build_job: true
|
||||||
path: anope
|
path: anope
|
||||||
refs:
|
refs:
|
||||||
stable: "2.0.9"
|
stable: "2.0.14"
|
||||||
release: "2.0.9"
|
release: "2.1.1"
|
||||||
devel: "2.0.9"
|
devel: "2.1"
|
||||||
devel_release: "2.0.9"
|
devel_release: "2.0"
|
||||||
build_script: |
|
build_script: |
|
||||||
cd $GITHUB_WORKSPACE/anope/
|
cd $GITHUB_WORKSPACE/anope/
|
||||||
cp $GITHUB_WORKSPACE/data/anope/* .
|
sudo apt-get install ninja-build --no-install-recommends
|
||||||
CFLAGS=-O0 ./Config -quick
|
mkdir build && cd build
|
||||||
make -C build -j 4
|
cmake -DCMAKE_INSTALL_PREFIX=$HOME/.local/ -DPROGRAM_NAME=anope -DUSE_PCH=ON -GNinja ..
|
||||||
make -C build install
|
ninja install
|
||||||
|
|
||||||
|
dlk:
|
||||||
|
name: Dlk
|
||||||
|
repository: DalekIRC/Dalek-Services
|
||||||
|
separate_build_job: false
|
||||||
|
path: Dlk-Services
|
||||||
|
refs:
|
||||||
|
stable: null # disabled because flaky, and hard to debug with all the PHP 8 warnings
|
||||||
|
release: &dlk_stable "6db51ea03f039c48fd20427c04cec8ff98df7878"
|
||||||
|
devel: "main"
|
||||||
|
devel_release: *dlk_stable
|
||||||
|
build_script: |
|
||||||
|
pip install pifpaf
|
||||||
|
wget -q https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
|
||||||
|
wget -q https://wordpress.org/latest.zip -O wordpress-latest.zip
|
||||||
|
env: >-
|
||||||
|
IRCTEST_DLK_PATH="${{ github.workspace }}/Dlk-Services"
|
||||||
|
IRCTEST_WP_CLI_PATH="${{ github.workspace }}/wp-cli.phar"
|
||||||
|
IRCTEST_WP_ZIP_PATH="${{ github.workspace }}/wordpress-latest.zip"
|
||||||
|
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
@ -332,13 +386,13 @@ software:
|
|||||||
install_steps:
|
install_steps:
|
||||||
stable:
|
stable:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pip install limnoria==2022.03.17 cryptography pyxmpp2-scram
|
run: pip install limnoria==2023.5.27 cryptography pyxmpp2-scram
|
||||||
release:
|
release:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
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@testing cryptography pyxmpp2-scram
|
run: pip install git+https://github.com/progval/Limnoria.git@master cryptography pyxmpp2-scram
|
||||||
devel_release: null
|
devel_release: null
|
||||||
|
|
||||||
sopel:
|
sopel:
|
||||||
@ -356,6 +410,23 @@ software:
|
|||||||
run: pip install git+https://github.com/sopel-irc/sopel.git
|
run: pip install git+https://github.com/sopel-irc/sopel.git
|
||||||
devel_release: null
|
devel_release: null
|
||||||
|
|
||||||
|
thelounge:
|
||||||
|
name: TheLounge
|
||||||
|
repository: thelounge/thelounge
|
||||||
|
separate_build_job: false
|
||||||
|
refs:
|
||||||
|
stable: "v4.4.0"
|
||||||
|
release: "v4.4.0"
|
||||||
|
devel: "master"
|
||||||
|
devel_release: null
|
||||||
|
path: thelounge
|
||||||
|
build_script: |
|
||||||
|
cd $GITHUB_WORKSPACE/thelounge
|
||||||
|
yarn install
|
||||||
|
NODE_ENV=production yarn build
|
||||||
|
mkdir -p ~/.local/bin/
|
||||||
|
ln -s $(pwd)/index.js ~/.local/bin/thelounge
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
bahamut:
|
bahamut:
|
||||||
software: [bahamut]
|
software: [bahamut]
|
||||||
@ -409,6 +480,9 @@ tests:
|
|||||||
nefarious:
|
nefarious:
|
||||||
software: [nefarious]
|
software: [nefarious]
|
||||||
|
|
||||||
|
sable:
|
||||||
|
software: [sable]
|
||||||
|
|
||||||
# doesn't build because it can't find liblex for some reason
|
# doesn't build because it can't find liblex for some reason
|
||||||
#snircd:
|
#snircd:
|
||||||
# software: [snircd]
|
# software: [snircd]
|
||||||
@ -425,9 +499,15 @@ tests:
|
|||||||
unrealircd-anope:
|
unrealircd-anope:
|
||||||
software: [unrealircd, anope]
|
software: [unrealircd, anope]
|
||||||
|
|
||||||
|
unrealircd-dlk:
|
||||||
|
software: [unrealircd, dlk]
|
||||||
|
|
||||||
|
|
||||||
limnoria:
|
limnoria:
|
||||||
software: [limnoria]
|
software: [limnoria]
|
||||||
|
|
||||||
sopel:
|
sopel:
|
||||||
software: [sopel]
|
software: [sopel]
|
||||||
|
|
||||||
|
thelounge:
|
||||||
|
software: [thelounge]
|
||||||
|
Reference in New Issue
Block a user