1
0
mirror of https://github.com/progval/irctest.git synced 2025-04-07 07:49:52 +00:00

2 Commits

Author SHA1 Message Date
0816232c1c Fix sync issue 2022-11-16 15:39:33 +01:00
3319920250 Check behavior of PRIVMSG when banned 2022-11-16 14:38:35 +01:00
38 changed files with 743 additions and 1528 deletions

@ -3,12 +3,12 @@
jobs:
build-anope:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
uses: actions/cache@v3
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-anope-devel
path: '~/.cache
@ -16,13 +16,13 @@ jobs:
${ github.workspace }/anope
'
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Anope
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: anope
ref: 2.0.9
@ -37,18 +37,18 @@ jobs:
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-anope
path: ~/artefacts-*.tar.gz
retention-days: 1
build-bahamut:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
uses: actions/cache@v3
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-bahamut-devel
path: '~/.cache
@ -56,13 +56,13 @@ jobs:
${ github.workspace }/Bahamut
'
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Bahamut
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: Bahamut
ref: master
@ -86,18 +86,18 @@ jobs:
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-bahamut.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-bahamut
path: ~/artefacts-*.tar.gz
retention-days: 1
build-hybrid:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
uses: actions/cache@v3
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-hybrid-devel
path: '~/.cache
@ -105,13 +105,13 @@ jobs:
${ github.workspace }/ircd-hybrid
'
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Hybrid
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: ircd-hybrid
ref: 8.2.x
@ -125,23 +125,23 @@ jobs:
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-hybrid.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-hybrid
path: ~/artefacts-*.tar.gz
retention-days: 1
build-inspircd:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout InspIRCd
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: inspircd
ref: master
@ -149,29 +149,25 @@ jobs:
- name: Build InspIRCd
run: |
cd $GITHUB_WORKSPACE/inspircd/
# Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch
./configure --prefix=$HOME/.local/inspircd --development
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
make -j 4
make install
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-inspircd
path: ~/artefacts-*.tar.gz
retention-days: 1
build-ngircd:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
uses: actions/cache@v3
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-ngircd-devel
path: '~/.cache
@ -179,13 +175,13 @@ jobs:
${ github.workspace }/ngircd
'
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout ngircd
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: ngircd
ref: master
@ -201,18 +197,18 @@ jobs:
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-ngircd.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-ngircd
path: ~/artefacts-*.tar.gz
retention-days: 1
build-plexus4:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
uses: actions/cache@v3
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-plexus4-devel
path: '~/.cache
@ -220,9 +216,9 @@ jobs:
${ github.workspace }/placeholder
'
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: clone
@ -243,18 +239,18 @@ jobs:
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-plexus4.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-plexus4
path: ~/artefacts-*.tar.gz
retention-days: 1
build-solanum:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
uses: actions/cache@v3
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-solanum-devel
path: '~/.cache
@ -262,13 +258,13 @@ jobs:
${ github.workspace }/solanum
'
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Solanum
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: solanum
ref: main
@ -283,18 +279,18 @@ jobs:
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-solanum.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-solanum
path: ~/artefacts-*.tar.gz
retention-days: 1
build-unrealircd:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
uses: actions/cache@v3
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-unrealircd-devel
path: '~/.cache
@ -302,13 +298,13 @@ jobs:
${ github.workspace }/unrealircd
'
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout UnrealIRCd 6
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: unrealircd
ref: unreal60_dev
@ -324,24 +320,23 @@ jobs:
CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick
make -j 4
make install
~/.local/unrealircd/unrealircd module install third/react
# Prevent download of geoIP database on first startup
sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-unrealircd.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-unrealircd
path: ~/artefacts-*.tar.gz
retention-days: 1
build-unrealircd-5:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
uses: actions/cache@v3
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-unrealircd-5-devel
path: '~/.cache
@ -349,13 +344,13 @@ jobs:
${ github.workspace }/unrealircd
'
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout UnrealIRCd 5
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: unrealircd
ref: unreal52
@ -371,13 +366,12 @@ jobs:
CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick
make -j 4
make install
~/.local/unrealircd/unrealircd module install third/react
# Prevent download of geoIP database on first startup
sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-unrealircd-5.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-unrealircd-5
path: ~/artefacts-*.tar.gz
@ -407,11 +401,11 @@ jobs:
- test-unrealircd-anope
- test-unrealircd-atheme
- test-unrealircd-dlk
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Download Artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
path: artifacts
- name: Install dashboard dependencies
@ -434,15 +428,15 @@ jobs:
test-bahamut:
needs:
- build-bahamut
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-bahamut
path: '~'
@ -460,7 +454,7 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_bahamut_devel
path: pytest.xml
@ -468,20 +462,20 @@ jobs:
needs:
- build-bahamut
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-bahamut
path: '~'
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-anope
path: '~'
@ -499,22 +493,22 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_bahamut-anope_devel
path: pytest.xml
test-bahamut-atheme:
needs:
- build-bahamut
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-bahamut
path: '~'
@ -532,21 +526,21 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_bahamut-atheme_devel
path: pytest.xml
test-ergo:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Ergo
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: ergo
ref: master
@ -572,7 +566,7 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_ergo_devel
path: pytest.xml
@ -580,20 +574,20 @@ jobs:
needs:
- build-hybrid
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-hybrid
path: '~'
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-anope
path: '~'
@ -611,22 +605,22 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_hybrid_devel
path: pytest.xml
test-inspircd:
needs:
- build-inspircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-inspircd
path: '~'
@ -644,7 +638,7 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd_devel
path: pytest.xml
@ -652,20 +646,20 @@ jobs:
needs:
- build-inspircd
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-inspircd
path: '~'
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-anope
path: '~'
@ -683,21 +677,21 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd-anope_devel
path: pytest.xml
test-ircu2:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout ircu2
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: ircu2
ref: u2_10_12_branch
@ -722,17 +716,17 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_ircu2_devel
path: pytest.xml
test-limnoria:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Install dependencies
@ -750,21 +744,21 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_limnoria_devel
path: pytest.xml
test-nefarious:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout nefarious
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: nefarious
ref: master
@ -788,22 +782,22 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_nefarious_devel
path: pytest.xml
test-ngircd:
needs:
- build-ngircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-ngircd
path: '~'
@ -821,7 +815,7 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_ngircd_devel
path: pytest.xml
@ -829,20 +823,20 @@ jobs:
needs:
- build-ngircd
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-ngircd
path: '~'
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-anope
path: '~'
@ -860,22 +854,22 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_ngircd-anope_devel
path: pytest.xml
test-ngircd-atheme:
needs:
- build-ngircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-ngircd
path: '~'
@ -893,7 +887,7 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_ngircd-atheme_devel
path: pytest.xml
@ -901,20 +895,20 @@ jobs:
needs:
- build-plexus4
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-plexus4
path: '~'
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-anope
path: '~'
@ -932,22 +926,22 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_plexus4_devel
path: pytest.xml
test-solanum:
needs:
- build-solanum
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-solanum
path: '~'
@ -965,17 +959,17 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_solanum_devel
path: pytest.xml
test-sopel:
needs: []
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Install dependencies
@ -992,22 +986,22 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_sopel_devel
path: pytest.xml
test-unrealircd:
needs:
- build-unrealircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-unrealircd
path: '~'
@ -1025,22 +1019,22 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd_devel
path: pytest.xml
test-unrealircd-5:
needs:
- build-unrealircd-5
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-unrealircd-5
path: '~'
@ -1058,7 +1052,7 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-5_devel
path: pytest.xml
@ -1066,20 +1060,20 @@ jobs:
needs:
- build-unrealircd
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-unrealircd
path: '~'
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-anope
path: '~'
@ -1097,22 +1091,22 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-anope_devel
path: pytest.xml
test-unrealircd-atheme:
needs:
- build-unrealircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-unrealircd
path: '~'
@ -1130,29 +1124,29 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-atheme_devel
path: pytest.xml
test-unrealircd-dlk:
needs:
- build-unrealircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
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@v3
uses: actions/checkout@v2
with:
path: Dlk-Services
ref: main
@ -1176,7 +1170,7 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_unrealircd-dlk_devel
path: pytest.xml

@ -3,12 +3,12 @@
jobs:
build-anope:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache dependencies
uses: actions/cache@v3
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-anope-devel_release
path: '~/.cache
@ -16,13 +16,13 @@ jobs:
${ github.workspace }/anope
'
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Anope
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: anope
ref: 2.0.9
@ -37,23 +37,23 @@ jobs:
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-anope
path: ~/artefacts-*.tar.gz
retention-days: 1
build-inspircd:
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout InspIRCd
uses: actions/checkout@v3
uses: actions/checkout@v2
with:
path: inspircd
ref: insp3
@ -61,18 +61,14 @@ jobs:
- name: Build InspIRCd
run: |
cd $GITHUB_WORKSPACE/inspircd/
# Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch
./configure --prefix=$HOME/.local/inspircd --development
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
make -j 4
make install
- name: Make artefact tarball
run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/
- name: Upload build artefacts
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: installed-inspircd
path: ~/artefacts-*.tar.gz
@ -84,11 +80,11 @@ jobs:
- test-inspircd
- test-inspircd-anope
- test-inspircd-atheme
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Download Artifacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
path: artifacts
- name: Install dashboard dependencies
@ -111,15 +107,15 @@ jobs:
test-inspircd:
needs:
- build-inspircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-inspircd
path: '~'
@ -137,7 +133,7 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd_devel_release
path: pytest.xml
@ -145,20 +141,20 @@ jobs:
needs:
- build-inspircd
- build-anope
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-inspircd
path: '~'
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-anope
path: '~'
@ -176,22 +172,22 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd-anope_devel_release
path: pytest.xml
test-inspircd-atheme:
needs:
- build-inspircd
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v4
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Download build artefacts
uses: actions/download-artifact@v3
uses: actions/download-artifact@v2
with:
name: installed-inspircd
path: '~'
@ -209,7 +205,7 @@ jobs:
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v2
with:
name: pytest-results_inspircd-atheme_devel_release
path: pytest.xml

File diff suppressed because it is too large Load Diff

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

@ -110,11 +110,8 @@ cd /tmp/
git clone https://github.com/inspircd/inspircd.git
cd inspircd
# Optional, makes tests run considerably faster. Pick one depending on the InspIRCd version:
# on Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21:
# optional, makes tests run considerably faster
patch src/inspircd.cpp < ~/irctest/patches/inspircd_mainloop.patch
# on Insp3 >= 3.17.0 and Insp4 >= 4.0.0a22:
export CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP
./configure --prefix=$HOME/.local/ --development
make -j 4

@ -1,27 +1,14 @@
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,
MutableMapping,
Optional,
Set,
Tuple,
Type,
)
from typing import IO, Any, Callable, Dict, List, Optional, Set, Tuple, Type
import irctest
@ -70,43 +57,9 @@ 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
@ -130,11 +83,6 @@ 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
@ -208,18 +156,10 @@ class DirectoryBasedController(_BaseController):
],
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-----
"""
)
)
subprocess.check_output(
[self.openssl_bin, "dhparam", "-out", self.dh_path, "128"],
stderr=subprocess.DEVNULL,
)
class BaseClientController(_BaseController):
@ -253,6 +193,9 @@ 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,
@ -261,6 +204,8 @@ 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()

@ -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,9 +448,7 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
print("{:.3f} S: {}".format(time.time(), line.strip()))
def readCapLs(
self,
auth: Optional[Authentication] = None,
tls_config: Optional[tls.TlsConfig] = None,
self, auth: Optional[Authentication] = None, tls_config: tls.TlsConfig = None
) -> None:
(hostname, port) = self.server.getsockname()
self.controller.run(
@ -460,9 +458,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:
@ -529,6 +527,8 @@ 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,6 +548,8 @@ 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,
@ -687,7 +689,7 @@ class BaseServerTestCase(
def connectClient(
self,
nick: str,
name: Optional[TClientName] = None,
name: TClientName = None,
capabilities: Optional[List[str]] = None,
skip_if_cap_nak: bool = False,
show_io: Optional[bool] = None,
@ -706,7 +708,7 @@ class BaseServerTestCase(
self.requestCapabilities(client, capabilities, skip_if_cap_nak)
if password is not None:
if "sasl" not in (capabilities or ()):
raise ValueError("Used 'password' option without sasl capbility")
raise ValueError("Used 'password' option without sasl capbilitiy")
self.authenticateClient(client, account or nick, password)
self.sendLine(client, "NICK {}".format(nick))
@ -732,8 +734,8 @@ class BaseServerTestCase(
self.server_support[param] = None
welcome.append(m)
self.targmax: Dict[str, Optional[str]] = dict( # type: ignore[assignment]
item.split(":", 1)
self.targmax: Dict[str, Optional[str]] = dict(
item.split(":", 1) # type: ignore
for item in (self.server_support.get("TARGMAX") or "").split(",")
if item
)

@ -3,7 +3,12 @@ import shutil
import subprocess
from typing import Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
global {{
@ -107,14 +112,21 @@ 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) = self.get_hostname_and_port()
(services_hostname, services_port) = self.get_hostname_and_port()
(unused_hostname, unused_port) = find_hostname_and_port()
(services_hostname, services_port) = find_hostname_and_port()
password_field = "passwd {};".format(password) if password else ""

@ -1,8 +1,13 @@
import shutil
import subprocess
from typing import Optional
from typing import Optional, Set
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_SSL_CONFIG = """
ssl_private_key = "{key_path}";
@ -36,13 +41,19 @@ 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) = self.get_hostname_and_port()
(services_hostname, services_port) = find_hostname_and_port()
password_field = 'password = "{}";'.format(password) if password else ""
if ssl:
self.gen_ssl()

@ -3,9 +3,13 @@ import json
import os
import shutil
import subprocess
from typing import Any, Dict, Optional, Type, Union
from typing import Any, Dict, Optional, Set, Type, Union
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.cases import BaseServerTestCase
BASE_CONFIG = {
@ -77,9 +81,6 @@ BASE_CONFIG = {
"channel-length": 128,
"client-length": 128,
"chathistory-maxmessages": 100,
"retention": {
"allow-individual-delete": True,
},
"tagmsg-storage": {
"default": False,
"whitelist": ["+draft/persist", "+persist"],
@ -129,7 +130,7 @@ def hash_password(password: Union[str, bytes]) -> str:
["ergo", "genpasswd"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
out, _ = p.communicate(input_)
return out.decode("utf-8").strip()
return out.decode("utf-8")
class ErgoController(BaseServerController, DirectoryBasedController):
@ -152,9 +153,17 @@ 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)

@ -1,5 +1,5 @@
import os
from typing import Optional, Tuple, Type
from typing import Optional, Set, Tuple, Type
from irctest.basecontrollers import BaseServerController
@ -39,6 +39,9 @@ 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

@ -1,9 +1,13 @@
import functools
import shutil
import subprocess
from typing import Optional, Type
from typing import Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
# Clients:
@ -76,8 +80,8 @@ TEMPLATE_CONFIG = """
# HELP/HELPOP
<module name="alias"> # for the HELP alias
<module name="{help_module_name}">
<include file="examples/{help_module_name}.conf.example">
<module name="helpop">
<include file="examples/helpop.conf.example">
# Misc:
<log method="file" type="*" level="debug" target="/tmp/ircd-{port}.log">
@ -90,17 +94,6 @@ 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"}
@ -120,13 +113,20 @@ 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) = self.get_hostname_and_port()
(services_hostname, services_port) = find_hostname_and_port()
password_field = 'password="{}"'.format(password) if password else ""
@ -138,13 +138,6 @@ 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(
@ -154,7 +147,6 @@ 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

@ -1,6 +1,6 @@
import shutil
import subprocess
from typing import Optional, Type
from typing import Optional, Set, Type
from irctest.basecontrollers import (
BaseServerController,
@ -49,8 +49,14 @@ 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:

@ -1,6 +1,6 @@
import shutil
import subprocess
from typing import Optional, Type
from typing import Optional, Set, Type
from irctest.basecontrollers import (
BaseServerController,
@ -68,8 +68,14 @@ 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:

@ -33,10 +33,10 @@ extensions:
- mammon.ext.ircv3.sasl
- mammon.ext.misc.nopost
metadata:
restricted_keys: []
restricted_keys:
{restricted_keys}
whitelist:
- display-name
- avatar
{authorized_keys}
monitor:
limit: 20
motd:
@ -89,6 +89,9 @@ 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:
@ -104,6 +107,8 @@ 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:

@ -2,7 +2,12 @@ import shutil
import subprocess
from typing import Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
[Global]
@ -48,13 +53,20 @@ 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) = self.get_hostname_and_port()
(unused_hostname, unused_port) = find_hostname_and_port()
password_field = "Password = {}".format(password) if password else ""

@ -1,6 +1,6 @@
import shutil
import subprocess
from typing import Optional, Type
from typing import Optional, Set, Type
from irctest.basecontrollers import (
BaseServerController,
@ -67,8 +67,14 @@ 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:

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

@ -5,15 +5,19 @@ from pathlib import Path
import shutil
import subprocess
import textwrap
from typing import Callable, ContextManager, Iterator, Optional, Type
from typing import Callable, ContextManager, Iterator, Optional, Set, Type
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
from irctest.basecontrollers import (
BaseServerController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.irc_utils.junkdrawer import find_hostname_and_port
TEMPLATE_CONFIG = """
include "modules.default.conf";
include "operclass.default.conf";
{extras}
loadmodule "third/redact";
include "help/help.conf";
me {{
@ -97,12 +101,7 @@ set {{
}}
modes-on-join "+H 100:1d"; // Enables CHATHISTORY
redacters {{
op;
sender;
}}
{set_v6only}
{set_extras}
}}
@ -125,24 +124,6 @@ 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"""
@ -205,8 +186,15 @@ 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
@ -219,54 +207,64 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
loadmodule "cloak_md5";
"""
)
set_v6only = SET_V6ONLY
set_extras = textwrap.indent(
textwrap.dedent(
"""
// Remove RPL_WHOISSPECIAL used to advertise security groups
whois-details {
security-groups { everyone none; self none; oper none; }
}
"""
),
" ",
)
else:
extras = ""
set_v6only = ""
set_extras = ""
with self.open_file("empty.txt") as fd:
fd.write("\n")
password_field = 'password "{}";'.format(password) if password else ""
(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)
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",
set_v6only=set_v6only,
extras=extras,
)
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
with _STARTSTOP_LOCK():
(services_hostname, services_port) = find_hostname_and_port()
(unused_hostname, unused_port) = find_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)
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,
)
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,

@ -39,7 +39,7 @@ class CaseResult:
type: Optional[str] = None
message: Optional[str] = None
def output_filename(self) -> str:
def output_filename(self):
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: Dict[str, str] = {}
extra = {}
for child in case:
if child.tag == "skipped":
success = True
@ -187,7 +187,7 @@ 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:
@ -220,7 +220,7 @@ 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()):
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
@ -314,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]
@ -366,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":
@ -379,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}")
@ -391,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

@ -18,7 +18,7 @@ class Artifact:
download_url: str
@property
def public_download_url(self) -> str:
def public_download_url(self):
# 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)

@ -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):

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

@ -7,14 +7,18 @@ and ban exception (`Modern <https://modern.ircdocs.horse/#exception-channel-mode
"""
from irctest import cases, runner
from irctest.numerics import ERR_BANNEDFROMCHAN, RPL_BANLIST, RPL_ENDOFBANLIST
from irctest.numerics import (
ERR_BANNEDFROMCHAN,
ERR_CANNOTSENDTOCHAN,
RPL_BANLIST,
RPL_ENDOFBANLIST,
)
from irctest.patma import ANYSTR, StrRe
class BanModeTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
def testBan(self):
"""Basic ban operation"""
def testBanJoin(self):
self.connectClient("chanop", name="chanop")
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")
@ -32,6 +36,55 @@ class BanModeTestCase(cases.BaseServerTestCase):
self.sendLine("bar", "JOIN #chan")
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")
@cases.mark_specifications("Modern")
def testBanPrivmsg(self):
"""
TODO: this checks the following quote is false:
"If `<target>` is a channel name and the client is [banned](#ban-channel-mode)
and not covered by a [ban exception](#ban-exception-channel-mode), the
message will not be delivered and the command will silently fail."
-- https://modern.ircdocs.horse/#privmsg-message
to check https://github.com/ircdocs/modern-irc/pull/201
"""
self.connectClient("chanop", name="chanop")
self.joinChannel("chanop", "#chan")
self.getMessages("chanop")
self.connectClient("Bar", name="bar")
self.getMessages("bar")
self.sendLine("bar", "JOIN #chan")
self.getMessages("bar")
self.getMessages("chanop")
self.sendLine("chanop", "MODE #chan +b bar!*@*")
self.assertMessageMatch(self.getMessage("chanop"), command="MODE")
self.getMessages("chanop")
self.getMessages("bar")
self.sendLine("bar", "PRIVMSG #chan :hello world")
self.assertMessageMatch(
self.getMessage("bar"),
command=ERR_CANNOTSENDTOCHAN,
params=["Bar", "#chan", ANYSTR],
)
self.assertEqual(self.getMessages("bar"), [])
self.assertEqual(self.getMessages("chanop"), [])
self.sendLine("chanop", "MODE #chan -b bar!*@*")
self.assertMessageMatch(self.getMessage("chanop"), command="MODE")
self.getMessages("chanop")
self.getMessages("bar")
self.sendLine("bar", "PRIVMSG #chan :hello again")
self.assertEqual(self.getMessages("bar"), [])
self.assertMessageMatch(
self.getMessage("chanop"),
command="PRIVMSG",
params=["#chan", "hello again"],
)
@cases.mark_specifications("Modern")
def testBanList(self):
"""`RPL_BANLIST <https://modern.ircdocs.horse/#rplbanlist-367>`_"""

@ -1,38 +0,0 @@
"""
Channel "no external messages" mode (`RFC 1459
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.3.1>`__,
`Modern <https://modern.ircdocs.horse/#no-external-messages-mode>`__)
"""
from irctest import cases
from irctest.numerics import ERR_CANNOTSENDTOCHAN
class NoExternalMessagesTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("RFC1459", "Modern")
def testNoExternalMessagesMode(self):
# test the +n channel mode
self.connectClient("chanop", name="chanop")
self.joinChannel("chanop", "#chan")
self.sendLine("chanop", "MODE #chan +n")
self.getMessages("chanop")
self.connectClient("baz", name="baz")
# this message should be suppressed completely by +n
self.sendLine("baz", "PRIVMSG #chan :hi from baz")
replies = self.getMessages("baz")
reply_cmds = {reply.command for reply in replies}
self.assertIn(ERR_CANNOTSENDTOCHAN, reply_cmds)
self.assertEqual(self.getMessages("chanop"), [])
# set the channel to -n: baz should be able to send now
self.sendLine("chanop", "MODE #chan -n")
replies = self.getMessages("chanop")
modeLines = [line for line in replies if line.command == "MODE"]
self.assertMessageMatch(modeLines[0], command="MODE", params=["#chan", "-n"])
self.sendLine("baz", "PRIVMSG #chan :hi again from baz")
self.getMessages("baz")
relays = self.getMessages("chanop")
self.assertMessageMatch(
relays[0], command="PRIVMSG", params=["#chan", "hi again from baz"]
)

@ -32,26 +32,6 @@ 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")
@ -100,13 +80,8 @@ class NoticeTestCase(cases.BaseServerTestCase):
class TagsTestCase(cases.BaseServerTestCase):
@cases.mark_capabilities("message-tags")
@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",
@cases.xfailIfSoftware(
["UnrealIRCd"], "https://bugs.unrealircd.org/view.php?id=5947"
)
def testLineTooLong(self):
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)

@ -6,8 +6,8 @@ from irctest import cases
class MetadataTestCase(cases.BaseServerTestCase):
valid_metadata_keys = {"display-name", "avatar"}
invalid_metadata_keys = {"indisplay-name", "inavatar"}
valid_metadata_keys = {"valid_key1", "valid_key2"}
invalid_metadata_keys = {"invalid_key1", "invalid_key2"}
@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 display-name")
self.sendLine(1, "METADATA * GET valid_key1")
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 display-name avatar")
self.sendLine(1, "METADATA * GET valid_key1 valid_key2")
m = self.getMessage(1)
self.assertMessageMatch(
m,
@ -62,10 +62,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
)
self.assertEqual(
m.params[1],
"display-name",
"valid_key1",
m,
fail_msg="Response to “METADATA * GET display-name avatar"
"did not respond to display-name first: {msg}",
fail_msg="Response to “METADATA * GET valid_key1 valid_key2"
"did not respond to valid_key1 first: {msg}",
)
m = self.getMessage(1)
self.assertMessageMatch(
@ -76,10 +76,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
)
self.assertEqual(
m.params[1],
"avatar",
"valid_key2",
m,
fail_msg="Response to “METADATA * GET display-name avatar"
"did not respond to avatar as second response: {msg}",
fail_msg="Response to “METADATA * GET valid_key1 valid_key2"
"did not respond to valid_key2 as second response: {msg}",
)
@cases.mark_specifications("IRCv3", deprecated=True)
@ -135,7 +135,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
)
self.assertEqual(
m.params[1],
"display-name",
"valid_key1",
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("*", "display-name", "myvalue")
self.assertSetGetValue("*", "valid_key1", "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("*", "display-name", "zero->\0<-zero", "zero->\\0<-zero")
self.assertSetGetValue("*", "valid_key1", "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(
"*",
"display-name",
"valid_key1",
"->{}<-".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 display-name " b":invalid UTF-8 ->\xc3<-\r\n"
b"METADATA * SET valid_key1 " 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 display-name " b":invalid UTF-8: \xc3\r\n"
b"METADATA * SET valid_key1 " b":invalid UTF-8: \xc3\r\n"
)
commands = {m.command for m in self.getMessages(1)}
self.assertNotIn(

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

@ -1,502 +0,0 @@
"""
`IRCv3 draft message redaction <https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md>`_
"""
import uuid
import pytest
from irctest import cases
from irctest.patma import ANYDICT, ANYSTR, StrRe
CAPABILITIES = [
"message-tags",
"echo-message",
"batch",
"server-time",
"labeled-response",
"draft/message-redaction",
]
@cases.mark_specifications("IRCv3")
@cases.mark_capabilities(*CAPABILITIES)
class ChannelRedactTestCase(cases.BaseServerTestCase):
def _setupRedactTest(self, redacteeId, redacteeNick, chathistory=False):
capabilities = list(CAPABILITIES)
if chathistory:
capabilities.extend(["batch", "draft/chathistory"])
self.connectClient("chanop", capabilities=capabilities, skip_if_cap_nak=True)
self.sendLine(1, "JOIN #chan")
self.connectClient("user", capabilities=capabilities, skip_if_cap_nak=True)
self.sendLine(2, "JOIN #chan")
self.getMessages(2) # synchronize
self.getMessages(1)
self.sendLine(redacteeId, "@label=1234 PRIVMSG #chan :hello there")
echo = self.getMessage(redacteeId)
self.assertMessageMatch(
echo,
tags={"label": "1234", "msgid": StrRe("[^ ]+"), **ANYDICT},
prefix=StrRe(redacteeNick + "!.*"),
command="PRIVMSG",
params=["#chan", "hello there"],
)
msgid = echo.tags["msgid"]
self.assertMessageMatch(
self.getMessage(3 - redacteeId),
tags={"msgid": msgid, **ANYDICT},
prefix=StrRe(redacteeNick + "!.*"),
command="PRIVMSG",
params=["#chan", "hello there"],
)
return msgid
def testRelayOpSelfRedact(self):
"""Channel op writes a message and redacts it themselves."""
msgid = self._setupRedactTest(redacteeId=1, redacteeNick="chanop")
self.sendLine(1, f"REDACT #chan {msgid} :oops")
self.assertMessageMatch(
self.getMessage(1),
prefix=StrRe("chanop!.*"),
command="REDACT",
params=["#chan", msgid, "oops"],
)
self.assertMessageMatch(
self.getMessage(2),
prefix=StrRe("chanop!.*"),
command="REDACT",
params=["#chan", msgid, "oops"],
)
def testRelayOpRedact(self):
"""User writes a message and channel op redacts it."""
msgid = self._setupRedactTest(
redacteeId=2,
redacteeNick="user",
)
self.sendLine(1, f"REDACT #chan {msgid} :spam")
self.assertMessageMatch(
self.getMessage(1),
prefix=StrRe("chanop!.*"),
command="REDACT",
params=["#chan", msgid, "spam"],
)
self.assertMessageMatch(
self.getMessage(2),
prefix=StrRe("chanop!.*"),
command="REDACT",
params=["#chan", msgid, "spam"],
)
def testRelayUserSelfRedact(self):
"""User writes a message and redacts it themselves.
Servers may either accept or reject this."""
msgid = self._setupRedactTest(redacteeId=2, redacteeNick="user")
self.sendLine(2, f"REDACT #chan {msgid} :oops")
msg = self.getMessage(2)
if msg.command == "REDACT":
self.assertMessageMatch(
msg,
prefix=StrRe("user!.*"),
command="REDACT",
params=["#chan", msgid, "oops"],
)
self.assertMessageMatch(
self.getMessage(1),
prefix=StrRe("user!.*"),
command="REDACT",
params=["#chan", msgid, "oops"],
)
else:
self.assertMessageMatch(
msg,
command="FAIL",
params=["REDACT", "REDACT_FORBIDDEN", "#chan", msgid, ANYSTR],
)
self.assertEqual(self.getMessages(1), [])
def testRejectRedactOtherUser(self):
"""Channel op writes a message and a user attempts to redact it."""
msgid = self._setupRedactTest(redacteeId=1, redacteeNick="chanop")
self.sendLine(2, f"REDACT #chan {msgid} :oops")
self.assertMessageMatch(
self.getMessage(2),
command="FAIL",
params=["REDACT", "REDACT_FORBIDDEN", "#chan", msgid, ANYSTR],
)
self.assertEqual(self.getMessages(1), [])
@pytest.mark.parametrize(
"chathistory_requester",
[
pytest.param(1, id="chathistory-to-chanop"),
pytest.param(2, id="chathistory-to-user"),
],
)
def testOpSelfRedactChathistory(self, chathistory_requester):
"""Channel op writes a message and redacts it themselves; both the op
and a regular user check the chathistory afterward.
https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md#chat-history
"""
msgid = self._setupRedactTest(
redacteeId=1, redacteeNick="chanop", chathistory=True
)
self.sendLine(1, f"REDACT #chan {msgid} :oops")
self.assertMessageMatch(
self.getMessage(1),
prefix=StrRe("chanop!.*"),
command="REDACT",
params=["#chan", msgid, "oops"],
)
self.getMessages(1)
self.getMessages(2)
self.sendLine(chathistory_requester, "CHATHISTORY LATEST #chan * 10")
(start_msg, *msgs, end_msg) = self.getMessages(chathistory_requester)
self.assertMessageMatch(
start_msg,
command="BATCH",
params=[StrRe(r"\+.+"), "chathistory", "#chan"],
)
batch_tag = start_msg.params[0][1:]
# remove Ergo's event-playback fallback
msgs = [msg for msg in msgs if not msg.prefix.startswith("HistServ!")]
self.assertMessageMatch(end_msg, command="BATCH", params=["-" + batch_tag])
if len(msgs) == 0:
pass # Server removed the message entirely
elif len(msgs) == 1:
# Server replaced with the REDACT
self.assertMessageMatch(
msgs[0],
prefix=StrRe("sender!.*"),
command="REDACT",
params=["#chan", msgid, "oops"],
)
elif len(msgs) == 2:
# Server appended the REDACT
self.assertMessageMatch(
msgs[0],
tags={"msgid": msgid, **ANYDICT},
command="PRIVMSG",
params=["#chan", msgid, "hello there"],
)
self.assertMessageMatch(
msgs[1],
prefix=StrRe("sender!.*"),
command="REDACT",
params=["#chan", msgid, "oops"],
)
else:
self.assertTrue(False, fail_msg=f"Unexpectedly many messages: {msgs}")
def testOpRedactNonExistant(self):
"""Channel op writes a message and redacts a random non-existant id."""
self._setupRedactTest(redacteeId=1, redacteeNick="chanop")
nonexistent_msgid = str(uuid.uuid4())
self.sendLine(1, f"REDACT #chan {nonexistent_msgid} :oops")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["REDACT", "UNKNOWN_MSGID", "#chan", nonexistent_msgid, ANYSTR],
)
self.assertEqual(self.getMessages(2), [])
def testOpRedactWrongChan(self):
"""Channel op writes a message and redacts it, but uses the wrong channel
as target."""
msgid = self._setupRedactTest(redacteeId=1, redacteeNick="chanop")
self.sendLine(1, "JOIN #otherChan")
self.getMessages(1)
self.sendLine(1, f"REDACT #otherChan {msgid} :oops")
msg = self.getMessage(1)
self.assertMessageMatch(
msg,
command="FAIL",
)
if msg.params[1] == "UNKNOWN_MSGID":
self.assertMessageMatch(
msg,
command="FAIL",
params=["REDACT", "UNKNOWN_MSGID", "#otherChan", msgid, ANYSTR],
)
else:
self.assertMessageMatch(
msg,
command="FAIL",
params=["REDACT", "REDACT_FORBIDDEN", "#otherChan", ANYSTR],
)
self.assertEqual(self.getMessages(2), [])
@cases.mark_specifications("IRCv3")
@cases.mark_capabilities(*CAPABILITIES)
@cases.mark_services
@pytest.mark.private_chathistory
class PmRedactTestCase(cases.BaseServerTestCase):
"""Tests REDACT command in private messages between authenticated accounts"""
def _setupRedactTest(self, chathistory=False):
capabilities = [*CAPABILITIES, "sasl"]
if chathistory:
capabilities.extend(["batch", "draft/chathistory"])
self.controller.registerUser(self, "sender", "senderpass")
self.controller.registerUser(self, "recipient", "recipientpass")
self.connectClient(
"sender",
password="senderpass",
capabilities=capabilities,
skip_if_cap_nak=True,
)
self.connectClient(
"recipient",
password="recipientpass",
capabilities=capabilities,
skip_if_cap_nak=True,
)
self.getMessages(2) # synchronize
self.getMessages(1)
self.sendLine(1, "@label=1234 PRIVMSG recipient :hello there")
echo = self.getMessage(1)
self.assertMessageMatch(
echo,
tags={"label": "1234", "msgid": StrRe("[^ ]+"), **ANYDICT},
prefix=StrRe("sender!.*"),
command="PRIVMSG",
params=["recipient", "hello there"],
)
msgid = echo.tags["msgid"]
self.assertMessageMatch(
self.getMessage(2),
tags={"msgid": msgid, **ANYDICT},
prefix=StrRe("sender!.*"),
command="PRIVMSG",
params=["recipient", "hello there"],
)
return msgid
def testRelaySenderRedact(self):
"""Someone writes a message in private and redacts it themselves."""
msgid = self._setupRedactTest()
self.sendLine(1, f"REDACT recipient {msgid} :oops")
self.assertMessageMatch(
self.getMessage(1),
prefix=StrRe("sender!.*"),
command="REDACT",
params=["recipient", msgid, "oops"],
)
self.assertMessageMatch(
self.getMessage(2),
prefix=StrRe("sender!.*"),
command="REDACT",
params=["recipient", msgid, "oops"],
)
def testRelayRecipientRedact(self):
"""Someone writes a message in private and their recipient redacts it.
Servers may either accept or reject this."""
msgid = self._setupRedactTest()
self.sendLine(2, f"REDACT sender {msgid} :oops")
msg = self.getMessage(2)
if msg.command == "REDACT":
self.assertMessageMatch(
msg,
prefix=StrRe("recipient!.*"),
command="REDACT",
params=["sender", msgid, "oops"],
)
self.assertMessageMatch(
self.getMessage(1),
prefix=StrRe("user!.*"),
command="REDACT",
params=["sender", msgid, "oops"],
)
else:
self.assertMessageMatch(
msg,
command="FAIL",
params=[
"REDACT",
StrRe("(REDACT_FORBIDDEN|UNKNOWN_MSGID)"),
"sender",
msgid,
ANYSTR,
],
)
self.assertEqual(self.getMessages(1), [])
@pytest.mark.parametrize("nick", ["sender", "recipient"])
def testRejectRedactOtherUser(self, nick):
"""Someone writes a message in private to someone else and an unrelated person
attempts to redact it."""
msgid = self._setupRedactTest()
self.controller.registerUser(self, "censor", "censorpass")
self.connectClient(
"censor",
password="censorpass",
capabilities=[*CAPABILITIES, "sasl"],
skip_if_cap_nak=True,
)
self.getMessages(3) # synchronize
self.sendLine(3, f"REDACT {nick} {msgid} :oops")
self.assertMessageMatch(
self.getMessage(3),
command="FAIL",
params=[
"REDACT",
StrRe("(REDACT_FORBIDDEN|UNKNOWN_MSGID)"),
nick,
msgid,
ANYSTR,
],
)
self.assertEqual(self.getMessages(1), [])
self.assertEqual(self.getMessages(2), [])
@pytest.mark.parametrize(
"chathistory_requester",
[
pytest.param(1, id="chathistory-to-sender"),
pytest.param(2, id="chathistory-to-recipient"),
],
)
@pytest.mark.private_chathistory
def testSenderRedactChathistory(self, chathistory_requester):
"""Channel op writes a message and redacts it themselves; both the op
and a regular user check the chathistory afterward.
https://github.com/progval/ircv3-specifications/blob/redaction/extensions/message-redaction.md#chat-history
"""
msgid = self._setupRedactTest(chathistory=True)
self.sendLine(1, f"REDACT recipient {msgid} :oops")
self.assertMessageMatch(
self.getMessage(1),
prefix=StrRe("sender!.*"),
command="REDACT",
params=["recipient", msgid, "oops"],
)
self.getMessages(1)
self.getMessages(2)
if chathistory_requester == 1:
others_nick = "recipient"
else:
others_nick = "sender"
self.sendLine(chathistory_requester, f"CHATHISTORY LATEST {others_nick} * 10")
(start_msg, *msgs, end_msg) = self.getMessages(chathistory_requester)
self.assertMessageMatch(
start_msg,
command="BATCH",
params=[StrRe(r"\+.+"), "chathistory", others_nick],
)
batch_tag = start_msg.params[0][1:]
# remove Ergo's event-playback fallback
msgs = [msg for msg in msgs if not msg.prefix.startswith("HistServ!")]
self.assertMessageMatch(end_msg, command="BATCH", params=["-" + batch_tag])
if len(msgs) == 0:
pass # Server removed the message entirely
elif len(msgs) == 1:
# Server replaced with the REDACT
self.assertMessageMatch(
msgs[0],
prefix=StrRe("sender!.*"),
command="REDACT",
params=["recipient", msgid, "oops"],
)
elif len(msgs) == 2:
# Server appended the REDACT
self.assertMessageMatch(
msgs[0],
tags={"msgid": msgid, **ANYDICT},
command="PRIVMSG",
params=["recipient", msgid, "hello there"],
)
self.assertMessageMatch(
msgs[1],
prefix=StrRe("sender!.*"),
command="REDACT",
params=["recipient", msgid, "oops"],
)
else:
self.assertTrue(False, fail_msg=f"Unexpectedly many messages: {msgs}")
def testRedactNonExistant(self):
"""Someone writes a message in private to someone else and redacts a random
non-existant id."""
self._setupRedactTest()
nonexistent_msgid = str(uuid.uuid4())
self.sendLine(1, f"REDACT recipient {nonexistent_msgid} :oops")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["REDACT", "UNKNOWN_MSGID", "recipient", nonexistent_msgid, ANYSTR],
)
self.assertEqual(self.getMessages(2), [])
def testOpRedactWrongChan(self):
"""Channel op writes a message and redacts it, but uses the wrong channel
as target."""
msgid = self._setupRedactTest()
self.sendLine(1, "JOIN #otherChan")
self.getMessages(1)
self.sendLine(1, f"REDACT #otherChan {msgid} :oops")
self.assertMessageMatch(
self.getMessage(1),
command="FAIL",
params=["REDACT", "UNKNOWN_MSGID", "#otherChan", msgid, ANYSTR],
)
self.assertEqual(self.getMessages(2), [])

@ -1,21 +1,36 @@
"""
`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, runner
from irctest import cases
from irctest.patma import ANYSTR
class Utf8TestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Ergo")
def testNonUtf8Filtering(self):
def testUtf8Validation(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),
@ -23,26 +38,3 @@ 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])

@ -99,12 +99,6 @@ 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"

@ -201,6 +201,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1967",
)
def testWhowasMultiple(self):
"""
"The history is searched backward, returning the most recent entry first."
@ -211,6 +215,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
def testWhowasCount1(self):
"""
"If there are multiple entries, up to <count> replies will be returned"
@ -221,6 +229,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
def testWhowasCount2(self):
"""
"If there are multiple entries, up to <count> replies will be returned"
@ -231,6 +243,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2")
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1968",
)
def testWhowasCountNegative(self):
"""
"If a non-positive number is passed as being <count>, then a full search
@ -248,6 +264,10 @@ class WhowasTestCase(cases.BaseServerTestCase):
@cases.xfailIfSoftware(
["ircu2"], "Fix not released yet: https://github.com/UndernetIRC/ircu2/pull/19"
)
@cases.xfailIfSoftware(
["InspIRCd"],
"Feature not released yet: https://github.com/inspircd/inspircd/pull/1967",
)
def testWhowasCountZero(self):
"""
"If a non-positive number is passed as being <count>, then a full search

@ -27,20 +27,16 @@ 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_REDACTION = "draft/message-redaction"
MESSAGE_TAGS = "message-tags"
MULTILINE = "draft/multiline"
MULTI_PREFIX = "multi-prefix"
SERVER_TIME = "server-time"
SETNAME = "setname"
STS = "sts"
@classmethod
@ -60,7 +56,6 @@ class IsupportTokens(enum.Enum):
MONITOR = "MONITOR"
STATUSMSG = "STATUSMSG"
TARGMAX = "TARGMAX"
UTF8ONLY = "UTF8ONLY"
WHOX = "WHOX"
@classmethod

@ -65,7 +65,7 @@ def get_install_steps(*, software_config, software_id, version_flavor):
install_steps = [
{
"name": f"Checkout {name}",
"uses": "actions/checkout@v3",
"uses": "actions/checkout@v2",
"with": {
"repository": software_config["repository"],
"ref": ref,
@ -94,7 +94,7 @@ def get_build_job(*, software_config, software_id, version_flavor):
cache = [
{
"name": "Cache dependencies",
"uses": "actions/cache@v3",
"uses": "actions/cache@v2",
"with": {
"path": f"~/.cache\n${{ github.workspace }}/{path}\n",
"key": "3-${{ runner.os }}-"
@ -116,17 +116,17 @@ def get_build_job(*, software_config, software_id, version_flavor):
return None
return {
"runs-on": "ubuntu-20.04",
"runs-on": "ubuntu-latest",
"steps": [
{
"name": "Create directories",
"run": "cd ~/; mkdir -p .local/ go/",
},
*cache,
{"uses": "actions/checkout@v3"},
{"uses": "actions/checkout@v2"},
{
"name": "Set up Python 3.7",
"uses": "actions/setup-python@v4",
"uses": "actions/setup-python@v2",
"with": {"python-version": 3.7},
},
*install_steps,
@ -159,7 +159,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
downloads.append(
{
"name": "Download build artefacts",
"uses": "actions/download-artifact@v3",
"uses": "actions/download-artifact@v2",
"with": {"name": f"installed-{software_id}", "path": "~"},
}
)
@ -191,13 +191,13 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
unpack = []
return {
"runs-on": "ubuntu-20.04",
"runs-on": "ubuntu-latest",
"needs": needs,
"steps": [
{"uses": "actions/checkout@v3"},
{"uses": "actions/checkout@v2"},
{
"name": "Set up Python 3.7",
"uses": "actions/setup-python@v4",
"uses": "actions/setup-python@v2",
"with": {"python-version": 3.7},
},
*downloads,
@ -231,7 +231,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
{
"name": "Publish results",
"if": "always()",
"uses": "actions/upload-artifact@v3",
"uses": "actions/upload-artifact@v2",
"with": {
"name": f"pytest-results_{test_id}_{version_flavor.value}",
"path": "pytest.xml",
@ -250,7 +250,7 @@ def upload_steps(software_id):
},
{
"name": "Upload build artefacts",
"uses": "actions/upload-artifact@v3",
"uses": "actions/upload-artifact@v2",
"with": {
"name": f"installed-{software_id}",
"path": "~/artefacts-*.tar.gz",
@ -263,6 +263,7 @@ 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}
@ -306,15 +307,15 @@ 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-20.04",
"runs-on": "ubuntu-latest",
# the build-and-test job might be skipped, we don't need to run
# this job then
"if": "success() || failure()",
"steps": [
{"uses": "actions/checkout@v3"},
{"uses": "actions/checkout@v2"},
{
"name": "Download Artifacts",
"uses": "actions/download-artifact@v3",
"uses": "actions/download-artifact@v2",
"with": {"path": "artifacts"},
},
{

@ -12,9 +12,6 @@ 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

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

@ -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:

@ -143,7 +143,7 @@ software:
name: InspIRCd
repository: inspircd/inspircd
refs: &inspircd_refs
stable: v3.15.0
stable: v3.12.0
release: null
devel: master
devel_release: insp3
@ -153,13 +153,9 @@ software:
separate_build_job: true
build_script: &inspircd_build_script |
cd $GITHUB_WORKSPACE/inspircd/
# Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true
patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch
./configure --prefix=$HOME/.local/inspircd --development
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
make -j 4
make install
irc2:
name: irc2
@ -272,8 +268,8 @@ software:
name: UnrealIRCd 6
repository: unrealircd/unrealircd
refs:
stable: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7
release: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7
stable: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3
release: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3
devel: unreal60_dev
devel_release: null
path: unrealircd
@ -289,7 +285,6 @@ software:
CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick
make -j 4
make install
~/.local/unrealircd/unrealircd module install third/react
# Prevent download of geoIP database on first startup
sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf