30 Commits

Author SHA1 Message Date
754df829d4 Add tests for SUB 2023-04-16 09:19:50 +02:00
10c1772121 Use real metadata keys 2023-04-16 09:19:50 +02:00
6c5595539c Add tests for KEY_NO_PERMISSION 2023-04-16 09:19:50 +02:00
34f078e04a Add a bunch of GetSet tests 2023-04-16 09:19:50 +02:00
f18162c02d Add untested 'before-connect' test 2023-04-16 09:19:50 +02:00
8a6a12e7f7 Fix fail code 2023-04-16 09:19:50 +02:00
0318a37075 Test for nick instead of * as target 2023-04-16 09:19:50 +02:00
8d01063aee Improve testing for invalid UTF8, and add test for too-long values 2023-04-16 09:19:50 +02:00
88f8be0ff6 expect batches 2023-04-16 09:19:50 +02:00
e17ab347f4 Adapt metadata_2 to stdreplies, and run it on Unreal
Passes on 3968354247
2023-04-16 09:19:50 +02:00
604041aa13 Copy metadata.py to metadata2.py 2023-04-16 09:19:50 +02:00
11dc5b046e unrealircd: Move SSL and port generation out of the critical section (#196) 2023-04-16 09:19:05 +02:00
ddb37d6c3f Use real metadata keys (#194) 2023-04-15 23:04:24 +02:00
aed6478a2c Bump UnrealIRCd to v6.0.7 (#192) 2023-04-05 08:24:34 +02:00
418b526033 Prevent random port collisions between controllers (#191)
This happens from time to time on the CI and is pretty annoying
2023-04-04 22:01:20 +02:00
136a7923c0 Bump linter versions (#188)
The isort we had has some weird poetry issue, I figured I might as well
bump the other linters at the same time

```
[INFO] Installing environment for https://github.com/PyCQA/isort.
[INFO] Once installed this environment will be reused.
[INFO] This may take a few minutes...
An unexpected error has occurred: CalledProcessError: command: ('/home/runner/.cache/pre-commit/repo0m3eczdf/py_env-python3.7/bin/python', '-mpip', 'install', '.')
return code: 1
stdout:
    Processing /home/runner/.cache/pre-commit/repo0m3eczdf
      Installing build dependencies: started
      Installing build dependencies: finished with status 'done'
      Getting requirements to build wheel: started
      Getting requirements to build wheel: finished with status 'done'
      Preparing metadata (pyproject.toml): started
      Preparing metadata (pyproject.toml): finished with status 'error'

stderr:
      error: subprocess-exited-with-error

      × Preparing metadata (pyproject.toml) did not run successfully.
      │ exit code: 1
      ╰─> [14 lines of output]
          Traceback (most recent call last):
            File "/home/runner/.cache/pre-commit/repo0m3eczdf/py_env-python3.7/lib/python3.7/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 353, in <module>
              main()
            File "/home/runner/.cache/pre-commit/repo0m3eczdf/py_env-python3.7/lib/python3.7/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 335, in main
              json_out['return_val'] = hook(**hook_input['kwargs'])
            File "/home/runner/.cache/pre-commit/repo0m3eczdf/py_env-python3.7/lib/python3.7/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 149, in prepare_metadata_for_build_wheel
              return hook(metadata_directory, config_settings)
            File "/tmp/pip-build-env-beaf5dxh/overlay/lib/python3.7/site-packages/poetry/core/masonry/api.py", line 40, in prepare_metadata_for_build_wheel
              poetry = Factory().create_poetry(Path(".").resolve(), with_groups=False)
            File "/tmp/pip-build-env-beaf5dxh/overlay/lib/python3.7/site-packages/poetry/core/factory.py", line 57, in create_poetry
              raise RuntimeError("The Poetry configuration is invalid:\n" + message)
          RuntimeError: The Poetry configuration is invalid:
            - [extras.pipfile_deprecated_finder.2] 'pip-shims<=0.3.4' does not match '^[a-zA-Z-_.0-9]+$'

          [end of output]

      note: This error originates from a subprocess, and is likely not a problem with pip.
    error: metadata-generation-failed

    × Encountered error while generating package metadata.
    ╰─> See above for output.

    note: This is an issue with the package mentioned above, not pip.
    hint: See above for details.
```
2023-03-04 10:51:40 +01:00
5364f963ae Add tests for draft/extended-monitor (#180) 2023-03-04 10:11:51 +01:00
1ea3e1c15c Fix insp4 support after 'helpop' config file was renamed (#187)
c2e954903a
2023-03-01 20:07:58 +01:00
8530c85adc sopel: remove use of deprecated argument
it's removed in aceedf5837
2023-02-15 19:11:51 +01:00
6815dd238b Fix race condition on Ergo 2023-02-11 22:26:23 +01:00
00562ff82d Run utf8 tests on servers which advertise UTF8ONLY (#185) 2023-01-28 10:12:32 +01:00
b7e8a7a5f5 direct message tests (#184)
* Test privmsg to non-existent user

* Test privmsg to user

* fix synchronization issue

* apply black

Co-authored-by: ma-anwar <ma.rizvi.anwar@gmail.com>
2023-01-22 07:45:25 -05:00
6181dd07ad Skip failure on RPL_WHOISSPECIAL with Dlk-Services 2022-12-16 19:09:09 +01:00
5fe4d4cfd8 github: Force ubuntu-20.04
Bahamut does not support ubuntu-22.04
2022-12-06 20:59:27 +01:00
544ca4b7ed Update flake8 URL
The Gitlab.com repo was removed today
2022-12-03 08:57:04 +01:00
35d342a478 account_registration: Add missing 'services' mark 2022-11-20 23:33:20 +01:00
29e4c2bbdb Hardcode DH parameters
openssl version in ubuntu 22.04 forbids moduli smaller than 512,
which would take longer to generate.
2022-11-18 18:57:51 +01:00
fd0b050686 Add support for Dlk-Services (#176) 2022-11-14 22:58:30 +01:00
d0645ab1a8 dashboard: Use qualified class names in multi-module views 2022-11-12 11:49:14 +01:00
65d7e0e506 whowas: Update quotes and links to Modern spec
In particular, this takes https://github.com/ircdocs/modern-irc/pull/196
into account.
2022-10-22 15:49:30 +02:00
40 changed files with 1662 additions and 350 deletions

View File

@ -3,7 +3,7 @@
jobs:
build-anope:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -43,7 +43,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-bahamut:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -92,7 +92,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-hybrid:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -131,7 +131,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-inspircd:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -162,7 +162,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-ngircd:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -203,7 +203,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-plexus4:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -245,7 +245,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-solanum:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -285,7 +285,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-unrealircd:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -331,7 +331,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-unrealircd-5:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -400,7 +400,8 @@ jobs:
- test-unrealircd-5
- test-unrealircd-anope
- test-unrealircd-atheme
runs-on: ubuntu-latest
- test-unrealircd-dlk
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Download Artifacts
@ -427,7 +428,7 @@ jobs:
test-bahamut:
needs:
- build-bahamut
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -461,7 +462,7 @@ jobs:
needs:
- build-bahamut
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -499,7 +500,7 @@ jobs:
test-bahamut-atheme:
needs:
- build-bahamut
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -531,7 +532,7 @@ jobs:
path: pytest.xml
test-ergo:
needs: []
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -573,7 +574,7 @@ jobs:
needs:
- build-hybrid
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -611,7 +612,7 @@ jobs:
test-inspircd:
needs:
- build-inspircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -645,7 +646,7 @@ jobs:
needs:
- build-inspircd
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -682,7 +683,7 @@ jobs:
path: pytest.xml
test-ircu2:
needs: []
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -721,7 +722,7 @@ jobs:
path: pytest.xml
test-limnoria:
needs: []
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -749,7 +750,7 @@ jobs:
path: pytest.xml
test-nefarious:
needs: []
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -788,7 +789,7 @@ jobs:
test-ngircd:
needs:
- build-ngircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -822,7 +823,7 @@ jobs:
needs:
- build-ngircd
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -860,7 +861,7 @@ jobs:
test-ngircd-atheme:
needs:
- build-ngircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -894,7 +895,7 @@ jobs:
needs:
- build-plexus4
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -932,7 +933,7 @@ jobs:
test-solanum:
needs:
- build-solanum
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -964,7 +965,7 @@ jobs:
path: pytest.xml
test-sopel:
needs: []
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -992,7 +993,7 @@ jobs:
test-unrealircd:
needs:
- build-unrealircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1025,7 +1026,7 @@ jobs:
test-unrealircd-5:
needs:
- build-unrealircd-5
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1059,7 +1060,7 @@ jobs:
needs:
- build-unrealircd
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1097,7 +1098,7 @@ jobs:
test-unrealircd-atheme:
needs:
- build-unrealircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1127,6 +1128,52 @@ jobs:
with:
name: pytest-results_unrealircd-atheme_devel
path: pytest.xml
test-unrealircd-dlk:
needs:
- build-unrealircd
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v2
with:
name: installed-unrealircd
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Checkout Dlk
uses: actions/checkout@v2
with:
path: Dlk-Services
ref: main
repository: DalekIRC/Dalek-Services
- name: Build Dlk
run: |
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
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
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" make unrealircd-dlk
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-dlk_devel
path: pytest.xml
name: irctest with devel versions
'on':
schedule:

View File

@ -3,7 +3,7 @@
jobs:
build-anope:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -43,7 +43,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-inspircd:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -80,7 +80,7 @@ jobs:
- test-inspircd
- test-inspircd-anope
- test-inspircd-atheme
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Download Artifacts
@ -107,7 +107,7 @@ jobs:
test-inspircd:
needs:
- build-inspircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -141,7 +141,7 @@ jobs:
needs:
- build-inspircd
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -179,7 +179,7 @@ jobs:
test-inspircd-atheme:
needs:
- build-inspircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7

View File

@ -3,7 +3,7 @@
jobs:
build-anope:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -43,7 +43,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-bahamut:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -92,7 +92,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-charybdis:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -132,7 +132,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-hybrid:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -171,7 +171,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-inspircd:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -202,7 +202,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-ngircd:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -243,7 +243,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-plexus4:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -285,7 +285,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-solanum:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -325,7 +325,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-unrealircd:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -347,7 +347,7 @@ jobs:
uses: actions/checkout@v2
with:
path: unrealircd
ref: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4
ref: da3c1c654481a33035b9c703957e1c25d0158259
repository: unrealircd/unrealircd
- name: Build UnrealIRCd 6
run: |
@ -371,7 +371,7 @@ jobs:
path: ~/artefacts-*.tar.gz
retention-days: 1
build-unrealircd-5:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
@ -443,7 +443,8 @@ jobs:
- test-unrealircd-5
- test-unrealircd-anope
- test-unrealircd-atheme
runs-on: ubuntu-latest
- test-unrealircd-dlk
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Download Artifacts
@ -470,7 +471,7 @@ jobs:
test-bahamut:
needs:
- build-bahamut
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -504,7 +505,7 @@ jobs:
needs:
- build-bahamut
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -542,7 +543,7 @@ jobs:
test-bahamut-atheme:
needs:
- build-bahamut
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -575,7 +576,7 @@ jobs:
test-charybdis:
needs:
- build-charybdis
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -607,7 +608,7 @@ jobs:
path: pytest.xml
test-ergo:
needs: []
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -649,7 +650,7 @@ jobs:
needs:
- build-hybrid
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -687,7 +688,7 @@ jobs:
test-inspircd:
needs:
- build-inspircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -721,7 +722,7 @@ jobs:
needs:
- build-inspircd
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -759,7 +760,7 @@ jobs:
test-inspircd-atheme:
needs:
- build-inspircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -791,7 +792,7 @@ jobs:
path: pytest.xml
test-irc2:
needs: []
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -841,7 +842,7 @@ jobs:
path: pytest.xml
test-ircu2:
needs: []
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -880,7 +881,7 @@ jobs:
path: pytest.xml
test-limnoria:
needs: []
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -907,7 +908,7 @@ jobs:
path: pytest.xml
test-nefarious:
needs: []
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -946,7 +947,7 @@ jobs:
test-ngircd:
needs:
- build-ngircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -980,7 +981,7 @@ jobs:
needs:
- build-ngircd
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1018,7 +1019,7 @@ jobs:
test-ngircd-atheme:
needs:
- build-ngircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1052,7 +1053,7 @@ jobs:
needs:
- build-plexus4
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1090,7 +1091,7 @@ jobs:
test-solanum:
needs:
- build-solanum
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1122,7 +1123,7 @@ jobs:
path: pytest.xml
test-sopel:
needs: []
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1150,7 +1151,7 @@ jobs:
test-unrealircd:
needs:
- build-unrealircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1183,7 +1184,7 @@ jobs:
test-unrealircd-5:
needs:
- build-unrealircd-5
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1217,7 +1218,7 @@ jobs:
needs:
- build-unrealircd
- build-anope
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1255,7 +1256,7 @@ jobs:
test-unrealircd-atheme:
needs:
- build-unrealircd
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
@ -1285,6 +1286,52 @@ jobs:
with:
name: pytest-results_unrealircd-atheme_stable
path: pytest.xml
test-unrealircd-dlk:
needs:
- build-unrealircd
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v2
with:
name: installed-unrealircd
path: '~'
- name: Unpack artefacts
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
- name: Checkout Dlk
uses: actions/checkout@v2
with:
path: Dlk-Services
ref: effd18652fc1c847d1959089d9cca9ff9837a8c0
repository: DalekIRC/Dalek-Services
- name: Build Dlk
run: |
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
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
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" make unrealircd-dlk
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-dlk_stable
path: pytest.xml
name: irctest with stable versions
'on':
pull_request: null

View File

@ -2,22 +2,23 @@ exclude: ^irctest/scram
repos:
- repo: https://github.com/psf/black
rev: 22.3.0
rev: 23.1.0
hooks:
- id: black
language_version: python3
- repo: https://github.com/PyCQA/isort
rev: 5.5.2
rev: 5.11.5
hooks:
- id: isort
- repo: https://gitlab.com/pycqa/flake8
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.812
rev: v1.0.1
hooks:
- id: mypy
additional_dependencies: [types-PyYAML, types-docutils]

View File

@ -274,3 +274,10 @@ unrealircd-anope:
--services-controller=irctest.controllers.anope_services \
-m 'services' \
-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)'

View File

@ -1,14 +1,27 @@
from __future__ import annotations
import dataclasses
import multiprocessing
import os
from pathlib import Path
import shutil
import socket
import subprocess
import tempfile
import textwrap
import time
from typing import IO, Any, Callable, Dict, List, Optional, Set, Tuple, Type
from typing import (
IO,
Any,
Callable,
Dict,
List,
MutableMapping,
Optional,
Set,
Tuple,
Type,
)
import irctest
@ -57,9 +70,43 @@ class _BaseController:
supported_sasl_mechanisms: Set[str]
proc: Optional[subprocess.Popen]
_used_ports: Set[Tuple[str, int]]
"""``(hostname, port))`` used by this controller."""
# the following need to be shared between processes in case we are running in
# parallel (with pytest-xdist)
# The dicts are used as a set of (hostname, port), because _manager.set() doesn't
# exist.
_manager = multiprocessing.Manager()
_port_lock = _manager.Lock()
"""Lock for access to ``_all_used_ports`` and ``_available_ports``."""
_all_used_ports: MutableMapping[Tuple[str, int], None] = _manager.dict()
"""``(hostname, port)`` used by all controllers."""
_available_ports: MutableMapping[Tuple[str, int], None] = _manager.dict()
"""``(hostname, port)`` available to any controller."""
def __init__(self, test_config: TestCaseControllerConfig):
self.test_config = test_config
self.proc = None
self._used_ports = set()
def get_hostname_and_port(self) -> Tuple[str, int]:
with self._port_lock:
try:
# try to get a known available port
((hostname, port), _) = self._available_ports.popitem()
except KeyError:
# if there aren't any, iterate while we get a fresh one.
while True:
(hostname, port) = find_hostname_and_port()
if (hostname, port) not in self._all_used_ports:
# double-checking in self._used_ports to prevent collisions
# between controllers starting at the same time.
break
# Make this port unavailable to other processes
self._all_used_ports[(hostname, port)] = None
return (hostname, port)
def check_is_alive(self) -> None:
assert self.proc
@ -83,6 +130,11 @@ class _BaseController:
if self.proc:
self.kill_proc()
# move this controller's ports from _all_used_ports to _available_ports
for hostname, port in self._used_ports:
del self._all_used_ports[(hostname, port)]
self._available_ports[(hostname, port)] = None
class DirectoryBasedController(_BaseController):
"""Helper for controllers whose software configuration is based on an
@ -156,10 +208,18 @@ class DirectoryBasedController(_BaseController):
],
stderr=subprocess.DEVNULL,
)
subprocess.check_output(
[self.openssl_bin, "dhparam", "-out", self.dh_path, "128"],
stderr=subprocess.DEVNULL,
)
with self.dh_path.open("w") as fd:
fd.write(
textwrap.dedent(
"""
-----BEGIN DH PARAMETERS-----
MIGHAoGBAJICSyQAiLj1fw8b5xELcnpqBQ+wvOyKgim4IetWOgZnRQFkTgOeoRZD
HksACRFJL/EqHxDKcy/2Ghwr2axhNxSJ+UOBmraP3WfodV/fCDPnZ+XnI9fjHsIr
rjisPMqomjXeiTB1UeAHvLUmCK4yx6lpAJsCYwJjsqkycUfHiy1bAgEC
-----END DH PARAMETERS-----
"""
)
)
class BaseClientController(_BaseController):
@ -193,9 +253,6 @@ class BaseServerController(_BaseController):
super().__init__(*args, **kwargs)
self.faketime_enabled = False
def get_hostname_and_port(self) -> Tuple[str, int]:
return find_hostname_and_port()
def run(
self,
hostname: str,
@ -204,8 +261,6 @@ class BaseServerController(_BaseController):
password: Optional[str],
ssl: bool,
run_services: bool,
valid_metadata_keys: Optional[Set[str]],
invalid_metadata_keys: Optional[Set[str]],
faketime: Optional[str],
) -> None:
raise NotImplementedError()
@ -301,10 +356,11 @@ class BaseServicesController(_BaseController):
c.sendLine("PONG :" + msg.params[0])
c.getMessages()
timeout = time.time() + 5
timeout = time.time() + 3
while True:
c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :HELP")
msgs = self.getNickServResponse(c)
c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :help")
msgs = self.getNickServResponse(c, timeout=1)
for msg in msgs:
if msg.command == "401":
# NickServ not available yet
@ -330,11 +386,12 @@ class BaseServicesController(_BaseController):
c.disconnect()
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
is queried asynchronously."""
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)
msgs = client.getMessages()
return msgs

View File

@ -173,7 +173,7 @@ class _IrcTestCase(Generic[TController]):
) -> Optional[str]:
"""Returns an error message if the message doesn't match the given arguments,
or None if it matches."""
for (key, value) in kwargs.items():
for key, value in kwargs.items():
if getattr(msg, key) != value:
fail_msg = (
fail_msg or "expected {param} to be {expects}, got {got}: {msg}"
@ -351,8 +351,8 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
nick: Optional[str] = None
user: Optional[List[str]] = None
server: socket.socket
protocol_version = Optional[str]
acked_capabilities = Optional[Set[str]]
protocol_version: Optional[str]
acked_capabilities: Optional[Set[str]]
__new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise
@ -448,7 +448,9 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
print("{:.3f} S: {}".format(time.time(), line.strip()))
def readCapLs(
self, auth: Optional[Authentication] = None, tls_config: tls.TlsConfig = None
self,
auth: Optional[Authentication] = None,
tls_config: Optional[tls.TlsConfig] = None,
) -> None:
(hostname, port) = self.server.getsockname()
self.controller.run(
@ -458,9 +460,9 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
m = self.getMessage()
self.assertEqual(m.command, "CAP", "First message is not CAP LS.")
if m.params == ["LS"]:
self.protocol_version = 301
self.protocol_version = "301"
elif m.params == ["LS", "302"]:
self.protocol_version = 302
self.protocol_version = "302"
elif m.params == ["END"]:
self.protocol_version = None
else:
@ -527,8 +529,6 @@ class BaseServerTestCase(
password: Optional[str] = None
ssl = False
valid_metadata_keys: Set[str] = set()
invalid_metadata_keys: Set[str] = set()
server_support: Optional[Dict[str, Optional[str]]]
run_services = False
@ -548,8 +548,6 @@ class BaseServerTestCase(
self.hostname,
self.port,
password=self.password,
valid_metadata_keys=self.valid_metadata_keys,
invalid_metadata_keys=self.invalid_metadata_keys,
ssl=self.ssl,
run_services=self.run_services,
faketime=self.faketime,
@ -689,7 +687,7 @@ class BaseServerTestCase(
def connectClient(
self,
nick: str,
name: TClientName = None,
name: Optional[TClientName] = None,
capabilities: Optional[List[str]] = None,
skip_if_cap_nak: bool = False,
show_io: Optional[bool] = None,
@ -734,8 +732,8 @@ class BaseServerTestCase(
self.server_support[param] = None
welcome.append(m)
self.targmax: Dict[str, Optional[str]] = dict(
item.split(":", 1) # type: ignore
self.targmax: Dict[str, Optional[str]] = dict( # type: ignore[assignment]
item.split(":", 1)
for item in (self.server_support.get("TARGMAX") or "").split(",")
if item
)

View File

@ -74,7 +74,19 @@ class ClientMock:
continue
if not synchronize:
got_pong = True
for line in data.decode().split("\r\n"):
try:
decoded_data = data.decode()
except UnicodeDecodeError:
print(
"{time:.3f}{ssl} S -> {client} - failed to decode: {data!r}".format(
time=time.time(),
ssl=" (ssl)" if self.ssl else "",
client=self.name,
data=data,
)
)
raise
for line in decoded_data.split("\r\n"):
if line:
if self.show_io:
print(

View File

@ -3,12 +3,7 @@ import shutil
import subprocess
from typing import Optional, Set, Type
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
TEMPLATE_CONFIG = """
global {{
@ -112,21 +107,14 @@ class BahamutController(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: 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:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None
self.port = port
self.hostname = hostname
self.create_config()
(unused_hostname, unused_port) = find_hostname_and_port()
(services_hostname, services_port) = find_hostname_and_port()
(unused_hostname, unused_port) = self.get_hostname_and_port()
(services_hostname, services_port) = self.get_hostname_and_port()
password_field = "passwd {};".format(password) if password else ""

View File

@ -1,13 +1,8 @@
import shutil
import subprocess
from typing import Optional, Set
from typing import Optional
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
TEMPLATE_SSL_CONFIG = """
ssl_private_key = "{key_path}";
@ -41,19 +36,13 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: bool,
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str],
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None
self.port = port
self.hostname = hostname
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 ""
if ssl:
self.gen_ssl()

View 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 "services.example.org";
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 = subprocess.Popen(
[
"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

View File

@ -3,13 +3,9 @@ import json
import os
import shutil
import subprocess
from typing import Any, Dict, Optional, Set, Type, Union
from typing import Any, Dict, Optional, Type, Union
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.cases import BaseServerTestCase
BASE_CONFIG = {
@ -153,17 +149,9 @@ class ErgoController(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: 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],
config: Optional[Any] = None,
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
self.create_config()
if config is None:
config = copy.deepcopy(BASE_CONFIG)

View File

@ -1,5 +1,5 @@
import os
from typing import Optional, Set, Tuple, Type
from typing import Optional, Tuple, Type
from irctest.basecontrollers import BaseServerController
@ -39,9 +39,6 @@ class ExternalServerController(BaseServerController):
password: Optional[str],
ssl: 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:
pass

View File

@ -1,13 +1,9 @@
import functools
import shutil
import subprocess
from typing import Optional, Set, Type
from typing import Optional, Type
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
TEMPLATE_CONFIG = """
# Clients:
@ -80,8 +76,8 @@ TEMPLATE_CONFIG = """
# HELP/HELPOP
<module name="alias"> # for the HELP alias
<module name="helpop">
<include file="examples/helpop.conf.example">
<module name="{help_module_name}">
<include file="examples/{help_module_name}.conf.example">
# Misc:
<log method="file" type="*" level="debug" target="/tmp/ircd-{port}.log">
@ -94,6 +90,17 @@ TEMPLATE_SSL_CONFIG = """
"""
@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
else:
assert False, f"unexpected version: {output}"
class InspircdController(BaseServerController, DirectoryBasedController):
software_name = "InspIRCd"
supported_sasl_mechanisms = {"PLAIN"}
@ -113,20 +120,13 @@ class InspircdController(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: 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,
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None
self.port = port
self.hostname = hostname
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 ""
@ -138,6 +138,13 @@ class InspircdController(BaseServerController, DirectoryBasedController):
else:
ssl_config = ""
if installed_version() == 3:
help_module_name = "helpop"
elif installed_version() == 4:
help_module_name = "help"
else:
assert False, f"unexpected version: {installed_version()}"
with self.open_file("server.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
@ -147,6 +154,7 @@ class InspircdController(BaseServerController, DirectoryBasedController):
services_port=services_port,
password_field=password_field,
ssl_config=ssl_config,
help_module_name=help_module_name,
)
)
assert self.directory

View File

@ -1,6 +1,6 @@
import shutil
import subprocess
from typing import Optional, Set, Type
from typing import Optional, Type
from irctest.basecontrollers import (
BaseServerController,
@ -49,14 +49,8 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: bool,
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str],
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
if ssl:
raise NotImplementedByController("TLS")
if run_services:

View File

@ -1,6 +1,6 @@
import shutil
import subprocess
from typing import Optional, Set, Type
from typing import Optional, Type
from irctest.basecontrollers import (
BaseServerController,
@ -68,14 +68,8 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: bool,
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str],
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
if ssl:
raise NotImplementedByController("TLS")
if run_services:

View File

@ -33,10 +33,10 @@ extensions:
- mammon.ext.ircv3.sasl
- mammon.ext.misc.nopost
metadata:
restricted_keys:
{restricted_keys}
restricted_keys: []
whitelist:
{authorized_keys}
- display-name
- avatar
monitor:
limit: 20
motd:
@ -89,9 +89,6 @@ class MammonController(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: 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:
if password is not None:
@ -107,8 +104,6 @@ class MammonController(BaseServerController, DirectoryBasedController):
directory=self.directory,
hostname=hostname,
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:

View File

@ -2,12 +2,7 @@ import shutil
import subprocess
from typing import Optional, Set, Type
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
TEMPLATE_CONFIG = """
[Global]
@ -53,20 +48,13 @@ class NgircdController(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: 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:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None
self.port = port
self.hostname = hostname
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 ""

View File

@ -1,6 +1,6 @@
import shutil
import subprocess
from typing import Optional, Set, Type
from typing import Optional, Type
from irctest.basecontrollers import (
BaseServerController,
@ -67,14 +67,8 @@ class SnircdController(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: bool,
run_services: bool,
valid_metadata_keys: Optional[Set[str]] = None,
invalid_metadata_keys: Optional[Set[str]] = None,
faketime: Optional[str],
) -> None:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
if ssl:
raise NotImplementedByController("TLS")
if run_services:

View File

@ -73,7 +73,7 @@ class SopelController(BaseClientController):
auth_method="auth_method = sasl" if auth else "",
)
)
self.proc = subprocess.Popen(["sopel", "--quiet", "-c", self.filename])
self.proc = subprocess.Popen(["sopel", "-c", self.filename])
def get_irctest_controller_class() -> Type[SopelController]:

View File

@ -5,17 +5,13 @@ from pathlib import Path
import shutil
import subprocess
import textwrap
from typing import Callable, ContextManager, Iterator, Optional, Set, Type
from typing import Callable, ContextManager, Iterator, Optional, Type
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
TEMPLATE_CONFIG = """
include "modules.default.conf";
loadmodule "third/metadata2";
include "operclass.default.conf";
{extras}
include "help/help.conf";
@ -101,7 +97,7 @@ set {{
}}
modes-on-join "+H 100:1d"; // Enables CHATHISTORY
{set_extras}
{set_v6only}
}}
@ -124,6 +120,24 @@ oper "operuser" {{
}}
"""
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]:
"""Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist"""
@ -186,15 +200,8 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
password: Optional[str],
ssl: 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:
if valid_metadata_keys or invalid_metadata_keys:
raise NotImplementedByController(
"Defining valid and invalid METADATA keys."
)
assert self.proc is None
self.port = port
self.hostname = hostname
@ -207,64 +214,54 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
loadmodule "cloak_md5";
"""
)
set_extras = textwrap.indent(
textwrap.dedent(
"""
// Remove RPL_WHOISSPECIAL used to advertise security groups
whois-details {
security-groups { everyone none; self none; oper none; }
}
"""
),
" ",
)
set_v6only = SET_V6ONLY
else:
extras = ""
set_extras = ""
set_v6only = ""
with self.open_file("empty.txt") as fd:
fd.write("\n")
password_field = 'password "{}";'.format(password) if password else ""
with _STARTSTOP_LOCK():
(services_hostname, services_port) = find_hostname_and_port()
(unused_hostname, unused_port) = find_hostname_and_port()
(services_hostname, services_port) = self.get_hostname_and_port()
(unused_hostname, unused_port) = self.get_hostname_and_port()
self.gen_ssl()
if ssl:
(tls_hostname, tls_port) = (hostname, port)
(hostname, port) = (unused_hostname, unused_port)
else:
# Unreal refuses to start without TLS enabled
(tls_hostname, tls_port) = (unused_hostname, unused_port)
self.gen_ssl()
if ssl:
(tls_hostname, tls_port) = (hostname, port)
(hostname, port) = (unused_hostname, unused_port)
else:
# Unreal refuses to start without TLS enabled
(tls_hostname, tls_port) = (unused_hostname, unused_port)
assert self.directory
assert self.directory
with self.open_file("unrealircd.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
hostname=hostname,
port=port,
services_hostname=services_hostname,
services_port=services_port,
tls_hostname=tls_hostname,
tls_port=tls_port,
password_field=password_field,
key_path=self.key_path,
pem_path=self.pem_path,
empty_file=self.directory / "empty.txt",
extras=extras,
set_extras=set_extras,
)
with self.open_file("unrealircd.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
hostname=hostname,
port=port,
services_hostname=services_hostname,
services_port=services_port,
tls_hostname=tls_hostname,
tls_port=tls_port,
password_field=password_field,
key_path=self.key_path,
pem_path=self.pem_path,
empty_file=self.directory / "empty.txt",
set_v6only=set_v6only,
extras=extras,
)
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
with _STARTSTOP_LOCK():
self.proc = subprocess.Popen(
[
*faketime_cmd,

View File

@ -39,7 +39,7 @@ class CaseResult:
type: Optional[str] = None
message: Optional[str] = None
def output_filename(self):
def output_filename(self) -> str:
test_name = self.test_name
if len(test_name) > 50 or set(test_name) & NETLIFY_CHAR_BLACKLIST:
# File name too long or otherwise invalid. This should be good enough:
@ -75,7 +75,7 @@ def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseR
skipped = False
details = None
system_out = None
extra = {}
extra: Dict[str, str] = {}
for child in case:
if child.tag == "skipped":
success = True
@ -173,6 +173,7 @@ def build_module_html(
def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element:
multiple_modules = len({r.module_name for r in results}) > 1
results_by_module_and_class = group_by(
results, lambda r: (r.module_name, r.class_name)
)
@ -186,22 +187,32 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element:
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()
):
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)
# Header row: class name
header_row = ET.SubElement(table, "tr")
th = ET.SubElement(header_row, "th", colspan=str(len(jobs) + 1))
row_anchor = f"{class_name}"
row_anchor = f"{qualified_class_name}"
section_header = ET.SubElement(
ET.SubElement(th, "h2"),
"a",
href=f"#{row_anchor}",
id=row_anchor,
)
section_header.text = class_name
section_header.text = qualified_class_name
append_docstring(th, getattr(module, class_name))
# Header row: one column for each implementation
@ -209,8 +220,8 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element:
# One row for each test:
results_by_test = group_by(class_results, key=lambda r: r.test_name)
for (test_name, test_results) in sorted(results_by_test.items()):
row_anchor = f"{class_name}.{test_name}"
for test_name, test_results in sorted(results_by_test.items()):
row_anchor = f"{qualified_class_name}.{test_name}"
if len(row_anchor) >= 50:
# Too long; give up on generating readable URL
# TODO: only hash test parameter
@ -292,7 +303,7 @@ def write_html_pages(
for result in results
)
assert is_client != is_server, (job, is_client, is_server)
if job.endswith(("-atheme", "-anope")):
if job.endswith(("-atheme", "-anope", "-dlk")):
assert is_server
job_categories[job] = "server-with-services"
elif is_server:
@ -303,7 +314,7 @@ def write_html_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
module_categories = {
job_categories[result.job]
@ -355,7 +366,7 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non
module_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":
module_pages.append((title, file_name))
elif page_type == "job":
@ -368,7 +379,7 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non
dl = ET.SubElement(body, "dl")
dl.set("class", "module-index")
for (module_name, file_name) in sorted(module_pages):
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}")
@ -380,7 +391,7 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non
ul = ET.SubElement(body, "ul")
ul.set("class", "job-index")
for (job, file_name) in sorted(job_pages):
for job, file_name in sorted(job_pages):
link = ET.SubElement(ET.SubElement(ul, "li"), "a", href=f"./{file_name}")
link.text = job

View File

@ -18,7 +18,7 @@ class Artifact:
download_url: str
@property
def public_download_url(self):
def public_download_url(self) -> str:
# GitHub API is not available publicly for artifacts, we need to use
# a third-party proxy to access it...
name = urllib.parse.quote(self.name)

View File

@ -152,7 +152,7 @@ def match_dict(
# Set to not-None if we find a Keys() operator in the dict keys
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):
remaining_keys_wildcard = (expected_key.key, expected_value)
else:
@ -168,7 +168,7 @@ def match_dict(
if 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):
return False
if not match_string(value, expected_value):

View File

@ -9,6 +9,7 @@ from irctest.patma import ANYSTR
REGISTER_CAP_NAME = "draft/account-registration"
@cases.mark_services
@cases.mark_specifications("IRCv3")
class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
@staticmethod
@ -33,6 +34,7 @@ class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
self.assertMessageMatch(register_response, params=["SUCCESS", ANYSTR, ANYSTR])
@cases.mark_services
@cases.mark_specifications("IRCv3")
class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
@staticmethod
@ -60,6 +62,7 @@ class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
)
@cases.mark_services
@cases.mark_specifications("IRCv3")
class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
@staticmethod
@ -110,6 +113,7 @@ class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
)
@cases.mark_services
@cases.mark_specifications("IRCv3", "Ergo")
class RegisterNoLandGrabsTestCase(cases.BaseServerTestCase):
@staticmethod

View File

@ -32,6 +32,26 @@ class PrivmsgTestCase(cases.BaseServerTestCase):
# ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL, or ERR_CANNOTSENDTOCHAN
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
self.assertIn(msg.command, ("401"))
class NoticeTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", "RFC2812")
@ -80,8 +100,13 @@ class NoticeTestCase(cases.BaseServerTestCase):
class TagsTestCase(cases.BaseServerTestCase):
@cases.mark_capabilities("message-tags")
@cases.xfailIfSoftware(
["UnrealIRCd"], "https://bugs.unrealircd.org/view.php?id=5947"
@cases.xfailIf(
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):
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)

View File

@ -6,8 +6,8 @@ from irctest import cases
class MetadataTestCase(cases.BaseServerTestCase):
valid_metadata_keys = {"valid_key1", "valid_key2"}
invalid_metadata_keys = {"invalid_key1", "invalid_key2"}
valid_metadata_keys = {"display-name", "avatar"}
invalid_metadata_keys = {"indisplay-name", "inavatar"}
@cases.mark_specifications("IRCv3", deprecated=True)
def testInIsupport(self):
@ -36,7 +36,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
def testGetOneUnsetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
self.connectClient("foo")
self.sendLine(1, "METADATA * GET valid_key1")
self.sendLine(1, "METADATA * GET display-name")
m = self.getMessage(1)
self.assertMessageMatch(
m,
@ -52,7 +52,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
"""
self.connectClient("foo")
self.sendLine(1, "METADATA * GET valid_key1 valid_key2")
self.sendLine(1, "METADATA * GET display-name avatar")
m = self.getMessage(1)
self.assertMessageMatch(
m,
@ -62,10 +62,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
)
self.assertEqual(
m.params[1],
"valid_key1",
"display-name",
m,
fail_msg="Response to “METADATA * GET valid_key1 valid_key2"
"did not respond to valid_key1 first: {msg}",
fail_msg="Response to “METADATA * GET display-name avatar"
"did not respond to display-name first: {msg}",
)
m = self.getMessage(1)
self.assertMessageMatch(
@ -76,10 +76,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
)
self.assertEqual(
m.params[1],
"valid_key2",
"avatar",
m,
fail_msg="Response to “METADATA * GET valid_key1 valid_key2"
"did not respond to valid_key2 as second response: {msg}",
fail_msg="Response to “METADATA * GET display-name avatar"
"did not respond to avatar as second response: {msg}",
)
@cases.mark_specifications("IRCv3", deprecated=True)
@ -135,7 +135,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
)
self.assertEqual(
m.params[1],
"valid_key1",
"display-name",
m,
fail_msg="Second param of 761 after setting “{expects}” to "
"{}” is not “{expects}”: {msg}.",
@ -190,7 +190,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
def testSetGetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient("foo")
self.assertSetGetValue("*", "valid_key1", "myvalue")
self.assertSetGetValue("*", "display-name", "myvalue")
@cases.mark_specifications("IRCv3", deprecated=True)
def testSetGetZeroCharInValue(self):
@ -198,7 +198,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
"""
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)
def testSetGetHeartInValue(self):
@ -209,7 +209,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
self.connectClient("foo")
self.assertSetGetValue(
"*",
"valid_key1",
"display-name",
"->{}<-".format(heart),
"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
# not like it
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)}
self.assertNotIn(
@ -233,7 +233,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
)
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)}
self.assertNotIn(

View File

@ -0,0 +1,617 @@
"""
`IRCv3 Metadata 2 <https://github.com/ircv3/ircv3-specifications/pull/501>`_
(not to be confused with the `deprecated IRCv3 Metadata
<https://ircv3.net/specs/core/metadata-3.2>`_)
"""
import itertools
import pytest
from irctest import cases, runner
from irctest.patma import ANYDICT, ANYLIST, ANYSTR, StrRe
CLIENT_NICKS = {
1: "foo",
2: "bar",
}
class MetadataTestCase(cases.BaseServerTestCase):
def getBatchMessages(self, client):
messages = self.getMessages(client)
first_msg = messages.pop(0)
last_msg = messages.pop(-1)
self.assertMessageMatch(
first_msg, command="BATCH", params=[StrRe(r"\+.*"), "metadata"]
)
batch_id = first_msg.params[0][1:]
self.assertMessageMatch(last_msg, command="BATCH", params=["-" + batch_id])
return (batch_id, messages)
def sub(self, client, keys):
self.sendLine(2, "METADATA * SUB " + " ".join(keys))
acknowledged_subs = []
for m in self.getMessages(2):
self.assertMessageMatch(
m,
command="770", # RPL_METADATASUBOK
params=["bar", *ANYLIST],
)
acknowledged_subs.extend(m.params[1:])
self.assertEqual(
sorted(acknowledged_subs),
sorted(keys),
fail_msg="Expected RPL_METADATASUBOK to ack {expects}, got {got}",
)
@cases.mark_specifications("IRCv3")
def testGetOneUnsetValid(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * GET display-name")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 1, fail_msg="Expected one ERR_NOMATCHINGKEY")
self.assertMessageMatch(
messages[0],
tags={"batch": batch_id, **ANYDICT},
command="766", # ERR_NOMATCHINGKEY
fail_msg="Did not reply with 766 (ERR_NOMATCHINGKEY) to a "
"request to an unset valid METADATA key: {msg}",
)
@cases.mark_specifications("IRCv3")
def testGetTwoUnsetValid(self):
"""“Multiple keys may be given. The response will be either RPL_KEYVALUE,
ERR_KEYINVALID or ERR_NOMATCHINGKEY for every key in order.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * GET display-name avatar")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 2, fail_msg="Expected two ERR_NOMATCHINGKEY")
self.assertMessageMatch(
messages[0],
command="766", # RPL_KEYNOTSET
fail_msg="Did not reply with 766 (RPL_KEYNOTSET) to a "
"request to two unset valid METADATA key: {msg}",
)
self.assertMessageMatch(
messages[0],
params=["foo", "foo", "display-name", ANYSTR],
fail_msg="Response to “METADATA * GET display-name avatar” "
"did not respond to display-name first: {msg}",
)
self.assertMessageMatch(
messages[1],
command="766", # RPL_KEYNOTSET
fail_msg="Did not reply with two 766 (RPL_KEYNOTSET) to a "
"request to two unset valid METADATA key: {msg}",
)
self.assertMessageMatch(
messages[1],
params=["foo", "foo", "avatar", ANYSTR],
fail_msg="Response to “METADATA * GET display-name avatar” "
"did not respond to avatar as second response: {msg}",
)
@cases.mark_specifications("IRCv3")
def testListNoSet(self):
"""“This subcommand MUST list all currently-set metadata keys along
with their values. The response will be zero or more RPL_KEYVALUE
events, following by RPL_METADATAEND event.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-list>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA * LIST")
(batch_id, messages) = self.getBatchMessages(1)
self.assertEqual(len(messages), 0, fail_msg="Expected empty batch")
@cases.mark_specifications("IRCv3")
def testListInvalidTarget(self):
"""“In case of invalid target RPL_METADATAEND MUST NOT be sent.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-list>
"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "METADATA foobar LIST")
m = self.getMessage(1)
self.assertMessageMatch(
m,
command="FAIL",
params=["METADATA", "INVALID_TARGET", "foobar", ANYSTR],
fail_msg="Response to “METADATA <invalid target> LIST” was "
"not 765 (ERR_TARGETINVALID) but: {msg}",
)
commands = {m.command for m in self.getMessages(1)}
self.assertNotIn(
"762",
commands,
fail_msg="Sent “METADATA <invalid target> LIST”, got FAIL INVALID_TARGET, "
"and then 762 (RPL_METADATAEND)",
)
def assertSetValue(self, client, target, key, value):
self.sendLine(client, "METADATA {} SET {} :{}".format(target, key, value))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
self.assertMessageMatch(
self.getMessage(client),
command="761", # RPL_KEYVALUE
params=[CLIENT_NICKS[client], target, key, ANYSTR, value],
)
def assertUnsetValue(self, client, target, key):
self.sendLine(client, "METADATA {} SET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
self.assertMessageMatch(
self.getMessage(client),
command="766", # RPL_KEYNOTSET
params=[CLIENT_NICKS[client], target, key, ANYSTR],
)
def assertGetValue(self, client, target, key, value):
self.sendLine(client, "METADATA {} GET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
(batch_id, messages) = self.getBatchMessages(client)
self.assertEqual(len(messages), 1, fail_msg="Expected one RPL_KEYVALUE")
self.assertMessageMatch(
messages[0],
command="761", # RPL_KEYVALUE
params=[CLIENT_NICKS[client], target, key, ANYSTR, value],
)
def assertValueNotSet(self, client, target, key):
self.sendLine(client, "METADATA {} GET {}".format(target, key))
if target == "*":
target = StrRe(r"(\*|" + CLIENT_NICKS[client] + ")")
(batch_id, messages) = self.getBatchMessages(client)
self.assertEqual(len(messages), 1, fail_msg="Expected one RPL_KEYVALUE")
self.assertMessageMatch(
messages[0],
command="766", # RPL_KEYNOTSET
params=[CLIENT_NICKS[client], target, key, ANYSTR],
)
def assertSetGetValue(self, client, target, key, value):
self.assertSetValue(client, target, key, value)
self.assertGetValue(client, target, key, value)
def assertUnsetGetValue(self, client, target, key):
self.assertUnsetValue(client, target, key)
self.assertValueNotSet(client, target, key)
@pytest.mark.parametrize(
"set_target,get_target", itertools.product(["*", "foo"], ["*", "foo"])
)
@cases.mark_specifications("IRCv3")
def testSetGetUser(self, set_target, get_target):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, set_target, "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetGetUserAgain(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertSetGetValue(1, "*", "display-name", "Foo The Second")
@cases.mark_specifications("IRCv3")
def testSetUnsetUser(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertUnsetGetValue(1, "*", "display-name")
@cases.mark_specifications("IRCv3")
def testGetOtherUser(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# As of 2023-04-15, the Unreal module requires users to share a channel for
# metadata to be visible to each other
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetValue(1, "*", "display-name", "Foo The First")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertGetValue(2, "foo", "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetOtherUser(self):
"""Not required by the spec, but it makes little sense to allow anyone to
write a channel's metadata"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# As of 2023-04-15, the Unreal module requires users to share a channel for
# metadata to be visible to each other.
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.sendLine(1, "METADATA bar SET display-name :Totally Not Foo")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["METADATA", "KEY_NO_PERMISSION", "bar", "display-name", ANYSTR],
)
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
@cases.mark_specifications("IRCv3")
def testSubUser(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["avatar", "display-name"])
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The First"],
)
self.assertSetGetValue(1, "*", "display-name", "Foo The Second")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The Second"],
)
self.assertUnsetGetValue(1, "*", "display-name")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["foo", "display-name", ANYSTR],
)
@cases.mark_specifications("IRCv3")
def testSubUserSetBeforeJoin(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["display-name", "avatar"])
self.assertSetGetValue(1, "*", "display-name", "Foo The First")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="'bar' got message when 'foo' set its display-name even though "
"they don't share a channel",
)
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.sendLine(2, "JOIN #chan")
messages = self.getMessages(2)
metadata_messages = [m for m in messages if m.command == "METADATA"]
self.assertEqual(
len(metadata_messages),
1,
fail_msg="Expected exactly one METADATA message when joining a channel, "
"got: {got}",
)
self.assertMessageMatch(
metadata_messages[0],
command="METADATA",
params=["foo", "display-name", ANYSTR, "Foo The First"],
)
@cases.mark_specifications("IRCv3")
def testSetGetChannel(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertGetValue(2, "#chan", "display-name", "Hash Channel")
@cases.mark_specifications("IRCv3")
def testSetUnsetChannel(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertUnsetGetValue(1, "#chan", "display-name")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
self.assertValueNotSet(2, "#chan", "display-name")
@cases.mark_specifications("IRCv3")
def testSetGetChannelNotOp(self):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.sendLine(2, "METADATA #chan SET display-name :Sharp Channel")
self.assertMessageMatch(
self.getMessage(2),
command="FAIL",
params=["METADATA", "KEY_NO_PERMISSION", "#chan", "display-name", ANYSTR],
)
self.assertEqual(
self.getMessages(1),
[],
fail_msg="Unexpected messages after other user used METADATA SET: {got}",
)
@cases.mark_specifications("IRCv3")
def testSubChannel(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["avatar", "display-name"])
self.sendLine(1, "JOIN #chan")
self.sendLine(2, "JOIN #chan")
self.getMessages(1)
self.getMessages(2)
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Hash Channel"],
)
self.assertSetGetValue(1, "#chan", "display-name", "Harsh Channel")
self.assertMessageMatch(
self.getMessage(2),
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Harsh Channel"],
)
@cases.mark_specifications("IRCv3")
def testSubChannelSetBeforeJoin(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.connectClient(
"bar", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.sub(2, ["display-name", "avatar"])
self.sendLine(1, "JOIN #chan")
self.getMessages(1)
self.assertSetGetValue(1, "#chan", "display-name", "Hash Channel")
self.assertEqual(
self.getMessages(2),
[],
fail_msg="'bar' got message when 'foo' set #chan's display-name even "
"though they are not in it",
)
self.sendLine(2, "JOIN #chan")
messages = self.getMessages(2)
metadata_messages = [m for m in messages if m.command == "METADATA"]
self.assertEqual(
len(metadata_messages),
1,
fail_msg="Expected exactly one METADATA message when joining a channel, "
"got: {got}",
)
self.assertMessageMatch(
metadata_messages[0],
command="METADATA",
params=["#chan", "display-name", ANYSTR, "Hash Channel"],
)
@cases.mark_specifications("IRCv3")
def testSetGetValidBeforeConnect(self):
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
self.addClient(1)
self.sendLine(1, "CAP LS 302")
caps = self.getCapLs(1)
if "before-connect" not in (caps["draft/metadata-2"] or "").split(","):
raise runner.OptionalExtensionNotSupported(
"draft/metadata-2=before-connect"
)
self.requestCapabilities(1, ["draft/metadata-2", "batch"], skip_if_cap_nak=True)
self.assertSetValue(1, "*", "display-name", "Foo The First")
self.sendLine(1, "NICK foo")
self.sendLine(1, "USER foo 0 * :foo")
self.sendLine(1, "CAP END")
self.skipToWelcome(1)
self.assertGetValue(1, "*", "display-name", "Foo The First")
@cases.mark_specifications("IRCv3")
def testSetGetHeartInValue(self):
"""“Values are unrestricted, except that they MUST be UTF-8.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
"""
heart = b"\xf0\x9f\x92\x9c".decode()
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
self.assertSetGetValue(
1,
"*",
"display-name",
"->{}<-".format(heart),
)
def _testSetInvalidValue(self, value):
self.connectClient(
"foo", capabilities=["draft/metadata-2", "batch"], skip_if_cap_nak=True
)
# Sending directly because it is not valid UTF-8 so Python would
# not like it
self.clients[1].conn.sendall(
b"METADATA * SET display-name :invalid UTF-8 ->\xc3<-\r\n"
)
try:
commands = {m.command for m in self.getMessages(1)}
except UnicodeDecodeError:
assert False, "Server sent invalid UTF-8"
self.assertNotIn(
"761",
commands, # RPL_KEYVALUE
fail_msg="Setting METADATA key to a value containing invalid "
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
)
self.clients[1].conn.sendall(b"METADATA * SET display-name :" + value + b"\r\n")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["METADATA", "VALUE_INVALID", ANYSTR],
)
messages = self.getMessages(1)
self.assertNotIn(
"761", # RPL_KEYVALUE
{m.command for m in messages},
fail_msg="Setting METADATA key to a value containing invalid "
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
)
self.assertEqual(
messages,
[],
fail_msg="Unexpected response to METADATA SET with invalid value: {got}",
)
@cases.mark_specifications("IRCv3")
def testSetInvalidUtf8(self):
"""“Values are unrestricted, except that they MUST be UTF-8.”
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
"""
self._testSetInvalidValue(b"invalid UTF-8: \xc3")
@cases.mark_specifications("IRCv3")
def testSetTooManyChars(self):
"""Assumes all servers reject values over 480 bytes. This isn't required by the
spec, but makes them risk overflowing when printing the value, so they probably
won't allow that.
"""
self._testSetInvalidValue(b"abcd" * 120)
@cases.mark_specifications("IRCv3")
def testSetTooManyBytes(self):
"""Assumes all servers reject values over 480 bytes. This isn't required by the
spec, but makes them risk overflowing when printing the value, so they probably
won't allow that.
"""
heart = b"\xf0\x9f\x92\x9c"
self._testSetInvalidValue(heart * 120)

View File

@ -1,7 +1,10 @@
"""
`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.client_mock import NoMessageException
from irctest.numerics import (
@ -13,7 +16,7 @@ from irctest.numerics import (
from irctest.patma import ANYSTR, StrRe
class MonitorTestCase(cases.BaseServerTestCase):
class _BaseMonitorTestCase(cases.BaseServerTestCase):
def check_server_support(self):
if "MONITOR" not in self.server_support:
raise runner.IsupportTokenNotSupported("MONITOR")
@ -42,6 +45,8 @@ class MonitorTestCase(cases.BaseServerTestCase):
extra_format=(nick,),
)
class MonitorTestCase(_BaseMonitorTestCase):
@cases.mark_specifications("IRCv3")
@cases.mark_isupport("MONITOR")
def testMonitorOneDisconnected(self):
@ -295,10 +300,11 @@ class MonitorTestCase(cases.BaseServerTestCase):
self.sendLine(2, "NICK qux")
self.getMessages(2)
mononline = self.getMessages(1)[0]
self.assertEqual(mononline.command, RPL_MONONLINE)
self.assertEqual(len(mononline.params), 2, mononline.params)
self.assertIn(mononline.params[0], ("bar", "*"))
self.assertEqual(mononline.params[1].split("!")[0], "qux")
self.assertMessageMatch(
mononline,
command=RPL_MONONLINE,
params=[StrRe(r"(bar|\*)"), StrRe("qux(!.*)?")],
)
# no numerics for a case change
self.sendLine(2, "NICK QUX")
@ -309,7 +315,246 @@ class MonitorTestCase(cases.BaseServerTestCase):
self.getMessages(2)
monoffline = self.getMessages(1)[0]
# should get RPL_MONOFFLINE with the current unfolded nick
self.assertEqual(monoffline.command, RPL_MONOFFLINE)
self.assertEqual(len(monoffline.params), 2, monoffline.params)
self.assertIn(monoffline.params[0], ("bar", "*"))
self.assertEqual(monoffline.params[1].split("!")[0], "QUX")
self.assertMessageMatch(
monoffline,
command=RPL_MONOFFLINE,
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=["draft/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")

View File

@ -178,6 +178,14 @@ class SaslTestCase(cases.BaseServerTestCase):
),
"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):
"""Test the client splits large AUTHENTICATE messages whose payload
is not a multiple of 400.

View File

@ -1,36 +1,21 @@
"""
`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>`_
"""
from irctest import cases
from irctest import cases, runner
from irctest.patma import ANYSTR
class Utf8TestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Ergo")
def testUtf8Validation(self):
def testNonUtf8Filtering(self):
self.connectClient(
"bar",
capabilities=["batch", "echo-message", "labeled-response"],
)
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.assertMessageMatch(
self.getMessage(1),
@ -38,3 +23,26 @@ class Utf8TestCase(cases.BaseServerTestCase):
params=["PRIVMSG", "INVALID_UTF8", ANYSTR],
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])

View File

@ -37,8 +37,8 @@ class BaseWhoTestCase:
self.sendLine(1, f"USER {self.username} 0 * :{self.realname}")
if auth:
self.sendLine(1, "CAP END")
self.getRegistrationMessage(1)
self.skipToWelcome(1)
self.getMessages(1)
self.sendLine(1, "JOIN #chan")
self.getMessages(1)

View File

@ -71,7 +71,10 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
last_message,
command=RPL_ENDOFWHOIS,
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 = []
@ -96,6 +99,12 @@ class _WhoisTestMixin(cases.BaseServerTestCase):
],
)
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
# explicit configuration by the operators.
assert False, "RPL_WHOISSPECIAL in use with default configuration"

View File

@ -98,7 +98,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"Servers MUST reply with either ERR_WASNOSUCHNICK or [...],
both followed with RPL_ENDOFWHOWAS"
-- https://github.com/ircdocs/modern-irc/pull/170
-- https://modern.ircdocs.horse/#whowas-message
"""
self.connectClient("nick1")
@ -210,7 +210,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"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/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")
@ -224,7 +224,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"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/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")
@ -238,7 +238,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"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/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")
@ -253,7 +253,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
is done."
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.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")
@ -271,7 +274,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
is done."
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.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")
@ -280,7 +286,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"""
"Wildcards are allowed in the <target> parameter."
-- 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":
raise runner.OptionalExtensionNotSupported("WHOWAS mask")
@ -324,7 +330,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"""
"If the `<nick>` argument is missing, they SHOULD send a single reply, using
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
# instead of ERR_NONICKNAMEGIVEN; and I couldn't find any that returns
@ -358,7 +364,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"""
https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.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:
@ -371,7 +377,7 @@ class WhowasTestCase(cases.BaseServerTestCase):
"Servers MUST reply with either ERR_WASNOSUCHNICK or [...],
both followed with RPL_ENDOFWHOWAS"
-- https://github.com/ircdocs/modern-irc/pull/170
-- https://modern.ircdocs.horse/#whowas-message
"""
self.connectClient("nick1")

View File

@ -27,16 +27,19 @@ class Specifications(enum.Enum):
@enum.unique
class Capabilities(enum.Enum):
ACCOUNT_NOTIFY = "account-notify"
ACCOUNT_TAG = "account-tag"
AWAY_NOTIFY = "away-notify"
BATCH = "batch"
ECHO_MESSAGE = "echo-message"
EXTENDED_JOIN = "extended-join"
EXTENDED_MONITOR = "extended-monitor"
LABELED_RESPONSE = "labeled-response"
MESSAGE_TAGS = "message-tags"
MULTILINE = "draft/multiline"
MULTI_PREFIX = "multi-prefix"
SERVER_TIME = "server-time"
SETNAME = "setname"
STS = "sts"
@classmethod
@ -56,6 +59,7 @@ class IsupportTokens(enum.Enum):
MONITOR = "MONITOR"
STATUSMSG = "STATUSMSG"
TARGMAX = "TARGMAX"
UTF8ONLY = "UTF8ONLY"
WHOX = "WHOX"
@classmethod

View File

@ -116,7 +116,7 @@ def get_build_job(*, software_config, software_id, version_flavor):
return None
return {
"runs-on": "ubuntu-latest",
"runs-on": "ubuntu-20.04",
"steps": [
{
"name": "Create directories",
@ -146,7 +146,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
for software_id in test_config.get("software", []):
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:
env += (
f"PATH={software_config['prefix']}/sbin"
@ -191,7 +191,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
unpack = []
return {
"runs-on": "ubuntu-latest",
"runs-on": "ubuntu-20.04",
"needs": needs,
"steps": [
{"uses": "actions/checkout@v2"},
@ -263,7 +263,6 @@ def upload_steps(software_id):
def generate_workflow(config: dict, version_flavor: VersionFlavor):
on: dict
if version_flavor == VersionFlavor.STABLE:
on = {"push": None, "pull_request": None}
@ -307,7 +306,7 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor):
jobs["publish-test-results"] = {
"name": "Publish Dashboard",
"needs": sorted({f"test-{test_id}" for test_id in config["tests"]} & set(jobs)),
"runs-on": "ubuntu-latest",
"runs-on": "ubuntu-20.04",
# the build-and-test job might be skipped, we don't need to run
# this job then
"if": "success() || failure()",

View File

@ -12,6 +12,9 @@ disallow_untyped_defs = False
[mypy-irctest.client_tests.*]
disallow_untyped_defs = False
[mypy-irctest.self_tests.*]
disallow_untyped_defs = False
[mypy-defusedxml.*]
ignore_missing_imports = True

View File

@ -18,16 +18,20 @@ markers =
private_chathistory
# capabilities
account-notify
account-tag
away-notify
batch
echo-message
extended-join
extended-monitor
labeled-response
message-tags
metadata-2
draft/multiline
multi-prefix
server-time
setname
sts
# isupport tokens
@ -38,6 +42,7 @@ markers =
PREFIX
STATUSMSG
TARGMAX
UTF8ONLY
WHOX
python_classes = *TestCase Test*

View File

@ -42,7 +42,7 @@ def partial_compaction(d):
# tests separate
compacted_d = {}
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:
successes.append((k, v))
else:

View File

@ -268,8 +268,8 @@ software:
name: UnrealIRCd 6
repository: unrealircd/unrealircd
refs:
stable: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3
release: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3
stable: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7
release: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7
devel: unreal60_dev
devel_release: null
path: unrealircd
@ -304,6 +304,7 @@ software:
#############################
# Services:
anope:
name: Anope
repository: anope/anope
@ -321,6 +322,24 @@ software:
make -C build -j 4
make -C build install
dlk:
name: Dlk
repository: DalekIRC/Dalek-Services
separate_build_job: false
path: Dlk-Services
refs:
stable: &dlk_stable "effd18652fc1c847d1959089d9cca9ff9837a8c0"
release: *dlk_stable
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"
#############################
@ -425,6 +444,9 @@ tests:
unrealircd-anope:
software: [unrealircd, anope]
unrealircd-dlk:
software: [unrealircd, dlk]
limnoria:
software: [limnoria]