mirror of
https://github.com/progval/irctest.git
synced 2025-04-06 23:39:46 +00:00
Compare commits
1 Commits
topic-unch
...
privmsg-se
Author | SHA1 | Date | |
---|---|---|---|
7b273443ef |
120
.github/deploy_to_netlify.py
vendored
120
.github/deploy_to_netlify.py
vendored
@ -1,120 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import pprint
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
event_name = os.environ["GITHUB_EVENT_NAME"]
|
|
||||||
|
|
||||||
is_pull_request = is_push = False
|
|
||||||
if event_name.startswith("pull_request"):
|
|
||||||
is_pull_request = True
|
|
||||||
elif event_name.startswith("push"):
|
|
||||||
is_push = True
|
|
||||||
elif event_name.startswith("schedule"):
|
|
||||||
# Don't publish; scheduled workflows run against the latest commit of every
|
|
||||||
# implementation, so they are likely to have failed tests for the wrong reasons
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
print("Unexpected event name:", event_name)
|
|
||||||
|
|
||||||
with open(os.environ["GITHUB_EVENT_PATH"]) as fd:
|
|
||||||
github_event = json.load(fd)
|
|
||||||
|
|
||||||
pprint.pprint(github_event)
|
|
||||||
|
|
||||||
context_suffix = ""
|
|
||||||
|
|
||||||
command = ["netlify", "deploy", "--dir=dashboard/"]
|
|
||||||
if is_pull_request:
|
|
||||||
pr_number = github_event["number"]
|
|
||||||
sha = github_event.get("after") or github_event["pull_request"]["head"]["sha"]
|
|
||||||
# Aliases can't exceed 37 chars
|
|
||||||
command.extend(["--alias", f"pr-{pr_number}-{sha[0:10]}"])
|
|
||||||
context_suffix = " (pull_request)"
|
|
||||||
elif is_push:
|
|
||||||
ref = github_event["ref"]
|
|
||||||
m = re.match("refs/heads/(.*)", ref)
|
|
||||||
if m:
|
|
||||||
branch = m.group(1)
|
|
||||||
sha = github_event["head_commit"]["id"]
|
|
||||||
|
|
||||||
if branch in ("main", "master"):
|
|
||||||
command.extend(["--prod"])
|
|
||||||
else:
|
|
||||||
command.extend(["--alias", f"br-{branch[0:23]}-{sha[0:10]}"])
|
|
||||||
context_suffix = " (push)"
|
|
||||||
else:
|
|
||||||
# TODO
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
print("Running", command)
|
|
||||||
proc = subprocess.run(command, capture_output=True)
|
|
||||||
|
|
||||||
output = proc.stdout.decode()
|
|
||||||
assert proc.returncode == 0, (output, proc.stderr.decode())
|
|
||||||
print(output)
|
|
||||||
|
|
||||||
m = re.search("https://[^ ]*--[^ ]*netlify.app", output)
|
|
||||||
assert m
|
|
||||||
netlify_site_url = m.group(0)
|
|
||||||
target_url = f"{netlify_site_url}/index.xhtml"
|
|
||||||
|
|
||||||
print("Published to", netlify_site_url)
|
|
||||||
|
|
||||||
|
|
||||||
def send_status() -> None:
|
|
||||||
statuses_url = github_event["repository"]["statuses_url"].format(sha=sha)
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"state": "success",
|
|
||||||
"context": f"Dashboard{context_suffix}",
|
|
||||||
"description": "Table of all test results",
|
|
||||||
"target_url": target_url,
|
|
||||||
}
|
|
||||||
request = urllib.request.Request(
|
|
||||||
statuses_url,
|
|
||||||
data=json.dumps(payload).encode(),
|
|
||||||
headers={
|
|
||||||
"Authorization": f'token {os.environ["GITHUB_TOKEN"]}',
|
|
||||||
"Content-Type": "text/json",
|
|
||||||
"Accept": "application/vnd.github+json",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
response = urllib.request.urlopen(request)
|
|
||||||
|
|
||||||
assert response.status == 201, response.read()
|
|
||||||
|
|
||||||
|
|
||||||
send_status()
|
|
||||||
|
|
||||||
|
|
||||||
def send_pr_comment() -> None:
|
|
||||||
comments_url = github_event["pull_request"]["_links"]["comments"]["href"]
|
|
||||||
|
|
||||||
payload = {
|
|
||||||
"body": f"[Test results]({target_url})",
|
|
||||||
}
|
|
||||||
request = urllib.request.Request(
|
|
||||||
comments_url,
|
|
||||||
data=json.dumps(payload).encode(),
|
|
||||||
headers={
|
|
||||||
"Authorization": f'token {os.environ["GITHUB_TOKEN"]}',
|
|
||||||
"Content-Type": "text/json",
|
|
||||||
"Accept": "application/vnd.github+json",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
response = urllib.request.urlopen(request)
|
|
||||||
|
|
||||||
assert response.status == 201, response.read()
|
|
||||||
|
|
||||||
|
|
||||||
if is_pull_request:
|
|
||||||
send_pr_comment()
|
|
801
.github/workflows/test-devel.yml
vendored
801
.github/workflows/test-devel.yml
vendored
File diff suppressed because it is too large
Load Diff
155
.github/workflows/test-devel_release.yml
vendored
155
.github/workflows/test-devel_release.yml
vendored
@ -3,32 +3,28 @@
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-anope:
|
build-anope:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
- name: Create directories
|
- name: Create directories
|
||||||
run: cd ~/; mkdir -p .local/ go/
|
run: cd ~/; mkdir -p .local/ go/
|
||||||
- name: Cache dependencies
|
- name: Cache Anope
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
key: 3-${{ runner.os }}-anope-devel_release
|
key: 3-${{ runner.os }}-anope-2.0.9
|
||||||
path: '~/.cache
|
path: '~/.cache
|
||||||
|
|
||||||
${ github.workspace }/anope
|
${{ github.workspace }}/anope
|
||||||
|
|
||||||
'
|
'
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Set up Python 3.11
|
|
||||||
uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: 3.11
|
|
||||||
- name: Checkout Anope
|
- name: Checkout Anope
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
path: anope
|
path: anope
|
||||||
ref: 2.0.9
|
ref: 2.0.9
|
||||||
repository: anope/anope
|
repository: anope/anope
|
||||||
- name: Build Anope
|
- name: Build Anope
|
||||||
run: |
|
run: |-
|
||||||
cd $GITHUB_WORKSPACE/anope/
|
cd $GITHUB_WORKSPACE/anope/
|
||||||
cp $GITHUB_WORKSPACE/data/anope/* .
|
cp $GITHUB_WORKSPACE/data/anope/* .
|
||||||
CFLAGS=-O0 ./Config -quick
|
CFLAGS=-O0 ./Config -quick
|
||||||
@ -37,23 +33,23 @@ jobs:
|
|||||||
- name: Make artefact tarball
|
- name: Make artefact tarball
|
||||||
run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/
|
run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/
|
||||||
- name: Upload build artefacts
|
- name: Upload build artefacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: installed-anope
|
name: installed-anope
|
||||||
path: ~/artefacts-*.tar.gz
|
path: ~/artefacts-*.tar.gz
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
build-inspircd:
|
build-inspircd:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Create directories
|
- name: Create directories
|
||||||
run: cd ~/; mkdir -p .local/ go/
|
run: cd ~/; mkdir -p .local/ go/
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.7
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.7
|
||||||
- name: Checkout InspIRCd
|
- name: Checkout InspIRCd
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
path: inspircd
|
path: inspircd
|
||||||
ref: insp3
|
ref: insp3
|
||||||
@ -61,72 +57,72 @@ jobs:
|
|||||||
- name: Build InspIRCd
|
- name: Build InspIRCd
|
||||||
run: |
|
run: |
|
||||||
cd $GITHUB_WORKSPACE/inspircd/
|
cd $GITHUB_WORKSPACE/inspircd/
|
||||||
|
patch src/inspircd.cpp < $GITHUB_WORKSPACE/inspircd_mainloop.patch
|
||||||
# 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
|
|
||||||
|
|
||||||
./configure --prefix=$HOME/.local/inspircd --development
|
./configure --prefix=$HOME/.local/inspircd --development
|
||||||
|
make -j 4
|
||||||
CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4
|
|
||||||
make install
|
make install
|
||||||
- name: Make artefact tarball
|
- name: Make artefact tarball
|
||||||
run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/
|
run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/
|
||||||
- name: Upload build artefacts
|
- name: Upload build artefacts
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: ~/artefacts-*.tar.gz
|
path: ~/artefacts-*.tar.gz
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
publish-test-results:
|
publish-test-results:
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
name: Publish Dashboard
|
name: Publish Unit Tests Results
|
||||||
needs:
|
needs:
|
||||||
- test-inspircd
|
- test-inspircd
|
||||||
- test-inspircd-anope
|
- test-inspircd-anope
|
||||||
- test-inspircd-atheme
|
- test-inspircd-atheme
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
- name: Download Artifacts
|
- name: Download Artifacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: Install dashboard dependencies
|
- if: github.event_name == 'pull_request'
|
||||||
run: |-
|
name: Publish Unit Test Results
|
||||||
python -m pip install --upgrade pip
|
uses: actions/github-script@v4
|
||||||
pip install defusedxml docutils -r requirements.txt
|
with:
|
||||||
- name: Generate dashboard
|
result-encoding: string
|
||||||
run: |-
|
script: |
|
||||||
shopt -s globstar
|
let body = '';
|
||||||
python3 -m irctest.dashboard.format dashboard/ artifacts/**/*.xml
|
const options = {};
|
||||||
echo '/ /index.xhtml' > dashboard/_redirects
|
options.listeners = {
|
||||||
- name: Install netlify-cli
|
stdout: (data) => {
|
||||||
run: npm i -g netlify-cli
|
body += data.toString();
|
||||||
- env:
|
}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
};
|
||||||
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options);
|
||||||
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
github.issues.createComment({
|
||||||
name: Deploy to Netlify
|
issue_number: context.issue.number,
|
||||||
run: ./.github/deploy_to_netlify.py
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
body: body,
|
||||||
|
});
|
||||||
|
return body;
|
||||||
test-inspircd:
|
test-inspircd:
|
||||||
needs:
|
needs:
|
||||||
- build-inspircd
|
- build-inspircd
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.7
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.7
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: '~'
|
path: '~'
|
||||||
- name: Unpack artefacts
|
- name: Unpack artefacts
|
||||||
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
|
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
|
||||||
- name: Install system dependencies
|
- name: Install Atheme
|
||||||
run: sudo apt-get install atheme-services faketime
|
run: sudo apt-get install atheme-services
|
||||||
- name: Install irctest dependencies
|
- name: Install irctest dependencies
|
||||||
run: |-
|
run: |-
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
@ -134,38 +130,37 @@ jobs:
|
|||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
|
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
|
||||||
make inspircd
|
make inspircd
|
||||||
timeout-minutes: 30
|
|
||||||
- if: always()
|
- if: always()
|
||||||
name: Publish results
|
name: Publish results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: pytest-results_inspircd_devel_release
|
name: pytest results inspircd (devel_release)
|
||||||
path: pytest.xml
|
path: pytest.xml
|
||||||
test-inspircd-anope:
|
test-inspircd-anope:
|
||||||
needs:
|
needs:
|
||||||
- build-inspircd
|
- build-inspircd
|
||||||
- build-anope
|
- build-anope
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.7
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.7
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: '~'
|
path: '~'
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: installed-anope
|
name: installed-anope
|
||||||
path: '~'
|
path: '~'
|
||||||
- name: Unpack artefacts
|
- name: Unpack artefacts
|
||||||
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
|
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
|
||||||
- name: Install system dependencies
|
- name: Install Atheme
|
||||||
run: sudo apt-get install atheme-services faketime
|
run: sudo apt-get install atheme-services
|
||||||
- name: Install irctest dependencies
|
- name: Install irctest dependencies
|
||||||
run: |-
|
run: |-
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
@ -173,32 +168,31 @@ jobs:
|
|||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make
|
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make
|
||||||
inspircd-anope
|
inspircd-anope
|
||||||
timeout-minutes: 30
|
|
||||||
- if: always()
|
- if: always()
|
||||||
name: Publish results
|
name: Publish results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: pytest-results_inspircd-anope_devel_release
|
name: pytest results inspircd-anope (devel_release)
|
||||||
path: pytest.xml
|
path: pytest.xml
|
||||||
test-inspircd-atheme:
|
test-inspircd-atheme:
|
||||||
needs:
|
needs:
|
||||||
- build-inspircd
|
- build-inspircd
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2
|
||||||
- name: Set up Python 3.11
|
- name: Set up Python 3.7
|
||||||
uses: actions/setup-python@v4
|
uses: actions/setup-python@v2
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
python-version: 3.7
|
||||||
- name: Download build artefacts
|
- name: Download build artefacts
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: installed-inspircd
|
name: installed-inspircd
|
||||||
path: '~'
|
path: '~'
|
||||||
- name: Unpack artefacts
|
- name: Unpack artefacts
|
||||||
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
|
run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \;
|
||||||
- name: Install system dependencies
|
- name: Install Atheme
|
||||||
run: sudo apt-get install atheme-services faketime
|
run: sudo apt-get install atheme-services
|
||||||
- name: Install irctest dependencies
|
- name: Install irctest dependencies
|
||||||
run: |-
|
run: |-
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
@ -206,12 +200,11 @@ jobs:
|
|||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
|
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
|
||||||
make inspircd-atheme
|
make inspircd-atheme
|
||||||
timeout-minutes: 30
|
|
||||||
- if: always()
|
- if: always()
|
||||||
name: Publish results
|
name: Publish results
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v2
|
||||||
with:
|
with:
|
||||||
name: pytest-results_inspircd-atheme_devel_release
|
name: pytest results inspircd-atheme (devel_release)
|
||||||
path: pytest.xml
|
path: pytest.xml
|
||||||
name: irctest with devel_release versions
|
name: irctest with devel_release versions
|
||||||
'on':
|
'on':
|
||||||
|
942
.github/workflows/test-stable.yml
vendored
942
.github/workflows/test-stable.yml
vendored
File diff suppressed because it is too large
Load Diff
@ -2,23 +2,22 @@ exclude: ^irctest/scram
|
|||||||
|
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 23.1.0
|
rev: 20.8b1
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
language_version: python3
|
language_version: python3
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/isort
|
- repo: https://github.com/PyCQA/isort
|
||||||
rev: 5.11.5
|
rev: 5.5.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: isort
|
- id: isort
|
||||||
|
|
||||||
- repo: https://github.com/PyCQA/flake8
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
rev: 5.0.4
|
rev: 3.8.3
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
|
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v1.0.1
|
rev: v0.812
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
additional_dependencies: [types-PyYAML, types-docutils]
|
|
||||||
|
142
Makefile
142
Makefile
@ -7,47 +7,74 @@ PYTEST_ARGS ?=
|
|||||||
# Will be appended at the end of the -k argument to pytest
|
# Will be appended at the end of the -k argument to pytest
|
||||||
EXTRA_SELECTORS ?=
|
EXTRA_SELECTORS ?=
|
||||||
|
|
||||||
|
# testPlainLarge fails because it doesn't handle split AUTHENTICATE (reported on IRC)
|
||||||
|
ANOPE_SELECTORS := \
|
||||||
|
and not testPlainLarge
|
||||||
|
|
||||||
|
# buffering tests cannot pass because of issues with UTF-8 handling: https://github.com/DALnet/bahamut/issues/196
|
||||||
BAHAMUT_SELECTORS := \
|
BAHAMUT_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
and not IRCv3 \
|
and not IRCv3 \
|
||||||
|
and not buffering \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
|
# testQuitErrors is very flaky
|
||||||
|
# AccountTagTestCase.testInvite fails because https://github.com/solanum-ircd/solanum/issues/166
|
||||||
|
# testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker.
|
||||||
|
# testWhoisNumerics[oper] fails because charybdis uses RPL_WHOISSPECIAL instead of RPL_WHOISOPERATOR
|
||||||
CHARYBDIS_SELECTORS := \
|
CHARYBDIS_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
|
and not testQuitErrors \
|
||||||
|
and not testKickDefaultComment \
|
||||||
|
and not (AccountTagTestCase and testInvite) \
|
||||||
|
and not (testWhoisNumerics and oper) \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
ERGO_SELECTORS := \
|
ERGO_SELECTORS := \
|
||||||
not deprecated \
|
not deprecated \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
|
# testInviteUnoppedModern is the only strict test that Hybrid fails
|
||||||
HYBRID_SELECTORS := \
|
HYBRID_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
|
and not testInviteUnoppedModern \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
|
# testNoticeNonexistentChannel fails because of https://github.com/inspircd/inspircd/issues/1849
|
||||||
|
# testBotPrivateMessage and testBotChannelMessage fail because https://github.com/inspircd/inspircd/pull/1910 is not released yet
|
||||||
|
# testNamesInvalidChannel and testNamesNonexistingChannel fail because https://github.com/inspircd/inspircd/pull/1922 is not released yet.
|
||||||
INSPIRCD_SELECTORS := \
|
INSPIRCD_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
|
and not testNoticeNonexistentChannel \
|
||||||
|
and not testBotPrivateMessage and not testBotChannelMessage \
|
||||||
|
and not testNamesInvalidChannel and not testNamesNonexistingChannel \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
# HelpTestCase fails because it returns NOTICEs instead of numerics
|
# buffering tests fail because ircu2 discards the whole buffer on long lines (TODO: refine how we exclude these tests)
|
||||||
|
# testQuit and testQuitErrors fail because ircu2 does not send ERROR or QUIT
|
||||||
|
# lusers tests fail because they depend on Modern behavior, not just RFC2812 (TODO: update lusers tests to accept RFC2812-compliant implementations)
|
||||||
|
# statusmsg tests fail because STATUSMSG is present in ISUPPORT, but it not actually supported as PRIVMSG target
|
||||||
|
# testKeyValidation[empty] fails because ircu2 returns ERR_NEEDMOREPARAMS on empty keys: https://github.com/UndernetIRC/ircu2/issues/13
|
||||||
|
# testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker.
|
||||||
|
# testEmptyRealname fails because it uses a default value instead of ERR_NEEDMOREPARAMS.
|
||||||
IRCU2_SELECTORS := \
|
IRCU2_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
$(EXTRA_SELECTORS)
|
and not buffering \
|
||||||
|
and not testQuit \
|
||||||
# same justification as ircu2
|
and not lusers \
|
||||||
# lusers "unregistered" tests fail because
|
and not statusmsg \
|
||||||
NEFARIOUS_SELECTORS := \
|
and not (testKeyValidation and empty) \
|
||||||
not Ergo \
|
and not testKickDefaultComment \
|
||||||
and not deprecated \
|
and not testEmptyRealname \
|
||||||
and not strict \
|
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
# same justification as ircu2
|
# same justification as ircu2
|
||||||
@ -55,12 +82,22 @@ SNIRCD_SELECTORS := \
|
|||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
|
and not buffering \
|
||||||
|
and not testQuit \
|
||||||
|
and not lusers \
|
||||||
|
and not statusmsg \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
|
# testListEmpty and testListOne fails because irc2 deprecated LIST
|
||||||
|
# testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker.
|
||||||
|
# testWallopsPrivileges fails because it ignores the command instead of replying ERR_UNKNOWNCOMMAND
|
||||||
IRC2_SELECTORS := \
|
IRC2_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
|
and not testListEmpty and not testListOne \
|
||||||
|
and not testKickDefaultComment \
|
||||||
|
and not testWallopsPrivileges \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
MAMMON_SELECTORS := \
|
MAMMON_SELECTORS := \
|
||||||
@ -69,14 +106,26 @@ MAMMON_SELECTORS := \
|
|||||||
and not strict \
|
and not strict \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
|
# testKeyValidation[spaces] and testKeyValidation[empty] fail because ngIRCd does not validate them https://github.com/ngircd/ngircd/issues/290
|
||||||
|
# testStarNick: wat
|
||||||
|
# testEmptyRealname fails because it uses a default value instead of ERR_NEEDMOREPARAMS.
|
||||||
|
# chathistory tests fail because they need nicks longer than 9 chars
|
||||||
NGIRCD_SELECTORS := \
|
NGIRCD_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
|
and not (testKeyValidation and (spaces or empty)) \
|
||||||
|
and not testStarNick \
|
||||||
|
and not testEmptyRealname \
|
||||||
|
and not chathistory \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
|
# testInviteUnoppedModern is the only strict test that Plexus4 fails
|
||||||
|
# testInviteInviteOnlyModern fails because Plexus4 allows non-op to invite if (and only if) the channel is not invite-only
|
||||||
PLEXUS4_SELECTORS := \
|
PLEXUS4_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
|
and not testInviteUnoppedModern \
|
||||||
|
and not testInviteInviteOnlyModern \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
@ -87,40 +136,47 @@ LIMNORIA_SELECTORS := \
|
|||||||
(foo or not foo) \
|
(foo or not foo) \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
|
# testQuitErrors is too flaky for CI
|
||||||
|
# testKickDefaultComment fails because solanum uses the nick of the kickee rather than the kicker.
|
||||||
SOLANUM_SELECTORS := \
|
SOLANUM_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
|
and not testQuitErrors \
|
||||||
|
and not testKickDefaultComment \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
# Same as Limnoria
|
|
||||||
SOPEL_SELECTORS := \
|
SOPEL_SELECTORS := \
|
||||||
(foo or not foo) \
|
not testPlainNotAvailable \
|
||||||
$(EXTRA_SELECTORS)
|
|
||||||
|
|
||||||
# TheLounge can actually pass all the test so there is none to exclude.
|
|
||||||
# `(foo or not foo)` serves as a `true` value so it doesn't break when
|
|
||||||
# $(EXTRA_SELECTORS) is non-empty
|
|
||||||
THELOUNGE_SELECTORS := \
|
|
||||||
(foo or not foo) \
|
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
|
# testNoticeNonexistentChannel fails: https://bugs.unrealircd.org/view.php?id=5949
|
||||||
|
# regressions::testTagCap fails: https://bugs.unrealircd.org/view.php?id=5948
|
||||||
|
# messages::testLineTooLong fails: https://bugs.unrealircd.org/view.php?id=5947
|
||||||
|
# testCapRemovalByClient and testNakWhole fail pending https://github.com/unrealircd/unrealircd/pull/148
|
||||||
# Tests marked with arbitrary_client_tags can't pass because Unreal whitelists which tags it relays
|
# Tests marked with arbitrary_client_tags can't pass because Unreal whitelists which tags it relays
|
||||||
# Tests marked with react_tag can't pass because Unreal blocks +draft/react https://github.com/unrealircd/unrealircd/pull/149
|
# Tests marked with react_tag can't pass because Unreal blocks +draft/react https://github.com/unrealircd/unrealircd/pull/149
|
||||||
# Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs
|
# Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs
|
||||||
|
# testChathistory[BETWEEN] fails: https://bugs.unrealircd.org/view.php?id=5952
|
||||||
|
# testChathistory[AROUND] fails: https://bugs.unrealircd.org/view.php?id=5953
|
||||||
UNREALIRCD_SELECTORS := \
|
UNREALIRCD_SELECTORS := \
|
||||||
not Ergo \
|
not Ergo \
|
||||||
and not deprecated \
|
and not deprecated \
|
||||||
and not strict \
|
and not strict \
|
||||||
|
and not testNoticeNonexistentChannel \
|
||||||
|
and not (regressions.py and testTagCap) \
|
||||||
|
and not (messages.py and testLineTooLong) \
|
||||||
|
and not (cap.py and (testCapRemovalByClient or testNakWhole)) \
|
||||||
|
and not (account_tag.py and testInvite) \
|
||||||
and not arbitrary_client_tags \
|
and not arbitrary_client_tags \
|
||||||
and not react_tag \
|
and not react_tag \
|
||||||
and not private_chathistory \
|
and not private_chathistory \
|
||||||
|
and not (testChathistory and (between or around)) \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd
|
.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon limnoria sopel solanum unrealircd
|
||||||
|
|
||||||
all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd
|
all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon limnoria sopel solanum unrealircd
|
||||||
|
|
||||||
flakes:
|
flakes:
|
||||||
find irctest/ -name "*.py" -not -path "irctest/scram/*" -print0 | xargs -0 pyflakes3
|
find irctest/ -name "*.py" -not -path "irctest/scram/*" -print0 | xargs -0 pyflakes3
|
||||||
@ -129,8 +185,7 @@ bahamut:
|
|||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller=irctest.controllers.bahamut \
|
--controller=irctest.controllers.bahamut \
|
||||||
-m 'not services' \
|
-m 'not services' \
|
||||||
-n 4 \
|
-n 10 \
|
||||||
-vv -s \
|
|
||||||
-k '$(BAHAMUT_SELECTORS)'
|
-k '$(BAHAMUT_SELECTORS)'
|
||||||
|
|
||||||
bahamut-atheme:
|
bahamut-atheme:
|
||||||
@ -138,6 +193,7 @@ bahamut-atheme:
|
|||||||
--controller=irctest.controllers.bahamut \
|
--controller=irctest.controllers.bahamut \
|
||||||
--services-controller=irctest.controllers.atheme_services \
|
--services-controller=irctest.controllers.atheme_services \
|
||||||
-m 'services' \
|
-m 'services' \
|
||||||
|
-n 10 \
|
||||||
-k '$(BAHAMUT_SELECTORS)'
|
-k '$(BAHAMUT_SELECTORS)'
|
||||||
|
|
||||||
bahamut-anope:
|
bahamut-anope:
|
||||||
@ -145,7 +201,8 @@ bahamut-anope:
|
|||||||
--controller=irctest.controllers.bahamut \
|
--controller=irctest.controllers.bahamut \
|
||||||
--services-controller=irctest.controllers.anope_services \
|
--services-controller=irctest.controllers.anope_services \
|
||||||
-m 'services' \
|
-m 'services' \
|
||||||
-k '$(BAHAMUT_SELECTORS)'
|
-n 10 \
|
||||||
|
-k '$(BAHAMUT_SELECTORS) $(ANOPE_SELECTORS)'
|
||||||
|
|
||||||
charybdis:
|
charybdis:
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
@ -161,7 +218,7 @@ ergo:
|
|||||||
hybrid:
|
hybrid:
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller irctest.controllers.hybrid \
|
--controller irctest.controllers.hybrid \
|
||||||
--services-controller=irctest.controllers.anope_services \
|
-m 'not services' \
|
||||||
-k "$(HYBRID_SELECTORS)"
|
-k "$(HYBRID_SELECTORS)"
|
||||||
|
|
||||||
inspircd:
|
inspircd:
|
||||||
@ -182,34 +239,27 @@ inspircd-anope:
|
|||||||
--controller=irctest.controllers.inspircd \
|
--controller=irctest.controllers.inspircd \
|
||||||
--services-controller=irctest.controllers.anope_services \
|
--services-controller=irctest.controllers.anope_services \
|
||||||
-m 'services' \
|
-m 'services' \
|
||||||
-k '$(INSPIRCD_SELECTORS)'
|
-k '$(INSPIRCD_SELECTORS) $(ANOPE_SELECTORS)'
|
||||||
|
|
||||||
ircu2:
|
ircu2:
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller=irctest.controllers.ircu2 \
|
--controller=irctest.controllers.ircu2 \
|
||||||
-m 'not services and not IRCv3' \
|
-m 'not services and not IRCv3' \
|
||||||
-n 4 \
|
-n 10 \
|
||||||
-k '$(IRCU2_SELECTORS)'
|
-k '$(IRCU2_SELECTORS)'
|
||||||
|
|
||||||
nefarious:
|
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
|
||||||
--controller=irctest.controllers.nefarious \
|
|
||||||
-m 'not services' \
|
|
||||||
-n 4 \
|
|
||||||
-k '$(NEFARIOUS_SELECTORS)'
|
|
||||||
|
|
||||||
snircd:
|
snircd:
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller=irctest.controllers.snircd \
|
--controller=irctest.controllers.snircd \
|
||||||
-m 'not services and not IRCv3' \
|
-m 'not services and not IRCv3' \
|
||||||
-n 4 \
|
-n 10 \
|
||||||
-k '$(SNIRCD_SELECTORS)'
|
-k '$(SNIRCD_SELECTORS)'
|
||||||
|
|
||||||
irc2:
|
irc2:
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller=irctest.controllers.irc2 \
|
--controller=irctest.controllers.irc2 \
|
||||||
-m 'not services and not IRCv3' \
|
-m 'not services and not IRCv3' \
|
||||||
-n 4 \
|
-n 10 \
|
||||||
-k '$(IRC2_SELECTORS)'
|
-k '$(IRC2_SELECTORS)'
|
||||||
|
|
||||||
limnoria:
|
limnoria:
|
||||||
@ -225,14 +275,14 @@ mammon:
|
|||||||
plexus4:
|
plexus4:
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller irctest.controllers.plexus4 \
|
--controller irctest.controllers.plexus4 \
|
||||||
--services-controller=irctest.controllers.anope_services \
|
-m 'not services' \
|
||||||
-k "$(PLEXUS4_SELECTORS)"
|
-k "$(PLEXUS4_SELECTORS)"
|
||||||
|
|
||||||
ngircd:
|
ngircd:
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller irctest.controllers.ngircd \
|
--controller irctest.controllers.ngircd \
|
||||||
-m 'not services' \
|
-m 'not services' \
|
||||||
-n 4 \
|
-n 10 \
|
||||||
-k "$(NGIRCD_SELECTORS)"
|
-k "$(NGIRCD_SELECTORS)"
|
||||||
|
|
||||||
ngircd-anope:
|
ngircd-anope:
|
||||||
@ -260,19 +310,12 @@ sopel:
|
|||||||
--controller=irctest.controllers.sopel \
|
--controller=irctest.controllers.sopel \
|
||||||
-k '$(SOPEL_SELECTORS)'
|
-k '$(SOPEL_SELECTORS)'
|
||||||
|
|
||||||
thelounge:
|
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
|
||||||
--controller=irctest.controllers.thelounge \
|
|
||||||
-k '$(THELOUNGE_SELECTORS)'
|
|
||||||
|
|
||||||
unrealircd:
|
unrealircd:
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller=irctest.controllers.unrealircd \
|
--controller=irctest.controllers.unrealircd \
|
||||||
-m 'not services' \
|
-m 'not services' \
|
||||||
-k '$(UNREALIRCD_SELECTORS)'
|
-k '$(UNREALIRCD_SELECTORS)'
|
||||||
|
|
||||||
unrealircd-5: unrealircd
|
|
||||||
|
|
||||||
unrealircd-atheme:
|
unrealircd-atheme:
|
||||||
$(PYTEST) $(PYTEST_ARGS) \
|
$(PYTEST) $(PYTEST_ARGS) \
|
||||||
--controller=irctest.controllers.unrealircd \
|
--controller=irctest.controllers.unrealircd \
|
||||||
@ -285,11 +328,4 @@ unrealircd-anope:
|
|||||||
--controller=irctest.controllers.unrealircd \
|
--controller=irctest.controllers.unrealircd \
|
||||||
--services-controller=irctest.controllers.anope_services \
|
--services-controller=irctest.controllers.anope_services \
|
||||||
-m 'services' \
|
-m 'services' \
|
||||||
-k '$(UNREALIRCD_SELECTORS)'
|
-k '$(UNREALIRCD_SELECTORS) $(ANOPE_SELECTORS)'
|
||||||
|
|
||||||
unrealircd-dlk:
|
|
||||||
pifpaf run mysql -- $(PYTEST) $(PYTEST_ARGS) \
|
|
||||||
--controller=irctest.controllers.unrealircd \
|
|
||||||
--services-controller=irctest.controllers.dlk_services \
|
|
||||||
-m 'services' \
|
|
||||||
-k '$(UNREALIRCD_SELECTORS)'
|
|
||||||
|
@ -18,11 +18,11 @@ have no side effect.
|
|||||||
Install irctest and dependencies:
|
Install irctest and dependencies:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt install faketime # Optional, but greatly speeds up irctest/server_tests/list.py
|
|
||||||
cd ~
|
cd ~
|
||||||
git clone https://github.com/ProgVal/irctest.git
|
git clone https://github.com/ProgVal/irctest.git
|
||||||
cd irctest
|
cd irctest
|
||||||
pip3 install --user -r requirements.txt
|
pip3 install --user -r requirements.txt
|
||||||
|
python3 setup.py install --user
|
||||||
```
|
```
|
||||||
|
|
||||||
Add `~/.local/bin/` (and/or `~/go/bin/` for Ergo)
|
Add `~/.local/bin/` (and/or `~/go/bin/` for Ergo)
|
||||||
@ -110,11 +110,8 @@ cd /tmp/
|
|||||||
git clone https://github.com/inspircd/inspircd.git
|
git clone https://github.com/inspircd/inspircd.git
|
||||||
cd inspircd
|
cd inspircd
|
||||||
|
|
||||||
# Optional, makes tests run considerably faster. Pick one depending on the InspIRCd version:
|
# optional, makes tests run considerably faster
|
||||||
# on Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21:
|
patch src/inspircd.cpp < ~/irctest/inspircd_mainloop.patch
|
||||||
patch src/inspircd.cpp < ~/irctest/patches/inspircd_mainloop.patch
|
|
||||||
# on Insp3 >= 3.17.0 and Insp4 >= 4.0.0a22:
|
|
||||||
export CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP
|
|
||||||
|
|
||||||
./configure --prefix=$HOME/.local/ --development
|
./configure --prefix=$HOME/.local/ --development
|
||||||
make -j 4
|
make -j 4
|
||||||
|
@ -106,10 +106,7 @@ def pytest_collection_modifyitems(session, config, items):
|
|||||||
assert isinstance(item, _pytest.python.Function)
|
assert isinstance(item, _pytest.python.Function)
|
||||||
|
|
||||||
# unittest-style test functions have the node of UnitTest class as parent
|
# unittest-style test functions have the node of UnitTest class as parent
|
||||||
if tuple(map(int, _pytest.__version__.split("."))) >= (7,):
|
assert isinstance(item.parent, _pytest.python.Instance)
|
||||||
assert isinstance(item.parent, _pytest.python.Class)
|
|
||||||
else:
|
|
||||||
assert isinstance(item.parent, _pytest.python.Instance)
|
|
||||||
|
|
||||||
# and that node references the UnitTest class
|
# and that node references the UnitTest class
|
||||||
assert issubclass(item.parent.cls, _IrcTestCase)
|
assert issubclass(item.parent.cls, _IrcTestCase)
|
||||||
|
@ -1,83 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDT0URxi7/l7ZGe
|
|
||||||
tkPv9Yh8h2s9BpbAR4Wq8sakgqETWg/nE/JQM5dPxroVbtZWWQXuJEFsgBKbASLa
|
|
||||||
/eg5cyJv4Uu5WIZpG1LxdPEEIOSMWjzoAGwoLxbTRGrS7qNXsknB9RwDuq8lPQiK
|
|
||||||
kiAahg1Cn1vRrQ4cRrG+AkQWpRHJEDoLjCSo8IcAsKAZlw/eGtAcmeNvkr5AujEw
|
|
||||||
XjIwx2FoDyKaNGRH5Z7gLWvCKBNxQuJuMTzh8guLqdGbE4hH3rqyICbW5DGPaOZL
|
|
||||||
LErWuJ7kEhLZG2HDW5JaXOr0QfFYAA8pl9/qCuFMdoxRUKRcYBoxoMmz6dlsmipN
|
|
||||||
7vIj+TT6TemwcAT25pwMJIVS4WC4+BZilNH2eWKD9hZA8Kq7FDPu+1rxOJaLbE/b
|
|
||||||
vpK8jZeRdqFzE1eBCgPkw8D8V0r7J18d+DsmgOe2kRycaia/t9M4rhqe0FXjX1X1
|
|
||||||
lzQ52grxgc28Ejd1fGQXIJmdTh4BqKqTzxup0izS7dgFP1Ezm6Z4O+wklpL5uQF2
|
|
||||||
Ex4X6QEj76iCxH+J/01/cvbxMe3iuGXECbO/y1FIrg7FKzZSrQo4aP63lS7Y7aq0
|
|
||||||
t2t6kOS83ebhnpgHClgFs8/C3ayzYBBtbK63PYthwO8Rt6WamCIZFF5tA3XoI4Ak
|
|
||||||
fZcWD18loZai+QznVzbLNINf++rTwwIDAQABAoICAQCs1tT3piZHU2yAyo9zLbJa
|
|
||||||
kxGxcT/v1CzBSmtG8ATJyrKxRzhxszdj9HABbzjcqrXJFbKA+5yy+OFdOtSUlFtk
|
|
||||||
Wb21lwPOnmo29sp4KPL1h+itEzMuMwZ4DBry1aFZvPSsnPpoHJwwUbY3hHdHzVzi
|
|
||||||
oTCGTqT188Wzmxu+MqHppCEJLSj45ZPzvyxU1UwwW0a4H+ZTM7WlEYlzw1lHLlpQ
|
|
||||||
VBFTLS8q77aNjOKiQptiz0X+zpS0dhJvu3l7BhwtMRS8prmqnffG4r0QWCsVPP8C
|
|
||||||
cbEJkWtbwswQikN6Xpi1yw6UTQZ8brZa810aOSh07EJTfrU35rjxAndEspbJPd+4
|
|
||||||
Zz6fKNnRA7A4fFap2hF4TSP/UF12b4ulQ8FfcMMTFszWko5M6fWFuCeWfNbyXML5
|
|
||||||
fmn+NmSOboj7LkDqbpxtyaIVXXb2Y3F6A2fNllm/mxaGrRoEGNaH3o0qBgWRzJJB
|
|
||||||
TDSvIQtJddzL+iMaqxz4ufXAREJernZmPa3vlkVGLINNQUC9JLrB5eFjLzPiZN2U
|
|
||||||
8RgQ9YX5tjoJ+DtPWuMFDiCS1ZE20/UBOEYTeqIVuKdK3AjJDMFSjg8fRvsWRqZe
|
|
||||||
zsHv6tCtIFZFxYRxtrRGTUPQF+1QD6zBjYxZZk1B4n3uYBGVQFM/LnNHUxRnJBx1
|
|
||||||
PunD4ICOY97xd2hcPmGiCQKCAQEA8NCXYaHzhv6fg95H/iMuJVcOCKrJ5rVr4poG
|
|
||||||
SD0KZtS7SLzUYat8WcuoSubh5Eb2hHtrsnLrSOTnwQUO61f4gCRm2sEqHYsOAd7+
|
|
||||||
mNe1jfil0UBVqqL9GBcGYJkc5+DHgUlJQaxMV+4YLt8fD0KfZEnHaDAYX3kUdz+p
|
|
||||||
be//YAKv+JmxWcUdBF60AUWPjbCJT/1pfJeY8nEBFiYtlYKKN24+4OiRdJ2yRGzt
|
|
||||||
ZtNHaWy5EFF70yVgPX5MGQ7Z2JpejzK+lt+9nG4h1uJ4M2X4YrGVrRCn1W8jwqm/
|
|
||||||
bXest3E6wkkLoWDm9EaeYj00DUgMOviPyP4ckyxilG+Fny4JbwKCAQEA4SyUV03X
|
|
||||||
KEoL5sOD69sLu3TpnIQz73u9an9W/f2f7rsGwmCcR9RdYGV29ltwfBvOm0FnPQio
|
|
||||||
GliN+3PTWAL6bb8VYo2IR53VKpVHKSQUlzDOD9PGObXw1CT/+0zoMP7FBA4dTJDf
|
|
||||||
xQ63AQNpSCGdwbxZygPWzLV5O1WxMeXhnQRL1EBvMyJ52od0+HbajDXg5mNiBKNQ
|
|
||||||
AtVhB9pEu47Blu/KBqWjfh/GeBLPZB7MHmGNBYbBGGskKRLG4sIbwShs9cx8UM0/
|
|
||||||
9dxXkX2d8hjnSD/0ZBh54HHUaEvKAKfpz1L8AC0ll7biCAy0CZK23AoZu/KT8zJ+
|
|
||||||
qvz3AuJcW3lo7QKCAQEAzfGltNJawPUamBzNttKBUV+s2c6tkkdO92C/xKGnNp/x
|
|
||||||
dtg+TTTpyKV5zGy9fIsPoecnCFptS06vwAvCYZQ/Kd93stcFXHSiSwlY9H9tfffK
|
|
||||||
XzTEzoRLLIHsa0omRUufcrqpEqf2NjChr9wS5OsWAx9xkHGpNmUHEqB4FlPsM0C5
|
|
||||||
G0LdQCdplGYlTP0fMo5qL+VJhErle1kXE8kcrMMRzyvSTGe4lWGTph79vDUt2kQn
|
|
||||||
1IPLAJzzPEO5cqiXtzz1Z0N/aOn5b0FkYTAWmeY30LeMiJA46Df+/ihLVKPHKq6E
|
|
||||||
EMmFT8LeYMPQCbXLwRv/kaMm3D4tU9PejpD9Vk95swKCAQAtULBlxXeIVyaAAVba
|
|
||||||
L1H0Hroo0n41MtzSwt+568G05JSep5yr4/QKw0CmoY5Im7v/iLEDGmviKXIhaZTd
|
|
||||||
wHOvhGYEWGFVsFDG6hXRFL7EEoFVtBPPZ2sY9n1BkJ+lxI/XmhORZhJycNypaotU
|
|
||||||
hddets4HFrCyr86+/ybS2OWHmOa9x13Zl5WYQexrWFfxIaKqGtQOBOPEPjbxwp5U
|
|
||||||
dI1HF+i7X7hAWJqzbW2pQ31mm9EqjIztoho73diCp/e37q/G46kdBcFadEZ3NCWG
|
|
||||||
JDbfVmeTgU19usq5Vo9HhINMQvIOAwfuuVJRtmTBDHKaY7n8FfxqU/4j4RbA0Ncv
|
|
||||||
XYadAoIBAQC7yh4/UZCGhklUhhk/667OfchWvWGriCSaYGmQPdMzxjnIjAvvIUe9
|
|
||||||
riOTBSZXYYLmZHsmY/jK7KMGB3AsLTypSa9+ddAWqWn2dvOYyxNiAaSJK/RZfA9A
|
|
||||||
ocVfvvkhOfNAYIF+A+fyJ2pznsDkBf9tPkhN7kovl+mr/e25qZb1d09377770Pi7
|
|
||||||
thzEi+JLrRgYVLrCrPi2j4l7/Va/UaAPz+Dtu2GCT9vXgnhZtpb8R1kTViZFryTv
|
|
||||||
k+qbNYJzVm61Vit9mVAGe+WuzhlclJnN6LIZGG3zYHIulRAJrH1XDauHZfHzCKgi
|
|
||||||
FnMesy4thDMH/MhUfRtbylZTq45gtvCA
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIFazCCA1OgAwIBAgIUYHD08+9S32VTD9IEsr2Oe1dH3VEwDQYJKoZIhvcNAQEL
|
|
||||||
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
|
|
||||||
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA0MDQxODE2NTZaFw0yMzA0
|
|
||||||
MDQxODE2NTZaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
|
|
||||||
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB
|
|
||||||
AQUAA4ICDwAwggIKAoICAQDT0URxi7/l7ZGetkPv9Yh8h2s9BpbAR4Wq8sakgqET
|
|
||||||
Wg/nE/JQM5dPxroVbtZWWQXuJEFsgBKbASLa/eg5cyJv4Uu5WIZpG1LxdPEEIOSM
|
|
||||||
WjzoAGwoLxbTRGrS7qNXsknB9RwDuq8lPQiKkiAahg1Cn1vRrQ4cRrG+AkQWpRHJ
|
|
||||||
EDoLjCSo8IcAsKAZlw/eGtAcmeNvkr5AujEwXjIwx2FoDyKaNGRH5Z7gLWvCKBNx
|
|
||||||
QuJuMTzh8guLqdGbE4hH3rqyICbW5DGPaOZLLErWuJ7kEhLZG2HDW5JaXOr0QfFY
|
|
||||||
AA8pl9/qCuFMdoxRUKRcYBoxoMmz6dlsmipN7vIj+TT6TemwcAT25pwMJIVS4WC4
|
|
||||||
+BZilNH2eWKD9hZA8Kq7FDPu+1rxOJaLbE/bvpK8jZeRdqFzE1eBCgPkw8D8V0r7
|
|
||||||
J18d+DsmgOe2kRycaia/t9M4rhqe0FXjX1X1lzQ52grxgc28Ejd1fGQXIJmdTh4B
|
|
||||||
qKqTzxup0izS7dgFP1Ezm6Z4O+wklpL5uQF2Ex4X6QEj76iCxH+J/01/cvbxMe3i
|
|
||||||
uGXECbO/y1FIrg7FKzZSrQo4aP63lS7Y7aq0t2t6kOS83ebhnpgHClgFs8/C3ayz
|
|
||||||
YBBtbK63PYthwO8Rt6WamCIZFF5tA3XoI4AkfZcWD18loZai+QznVzbLNINf++rT
|
|
||||||
wwIDAQABo1MwUTAdBgNVHQ4EFgQU+9eHi2eqy0f3fDS0GjqkijGDDocwHwYDVR0j
|
|
||||||
BBgwFoAU+9eHi2eqy0f3fDS0GjqkijGDDocwDwYDVR0TAQH/BAUwAwEB/zANBgkq
|
|
||||||
hkiG9w0BAQsFAAOCAgEAAJXO3qUc/PW75pI2dt1cKv20VqozkfEf7P0eeVisCDxn
|
|
||||||
1p3QhVgI2lEe9kzdHp7t42g5xLkUhQEVmBcKm9xbl+k2D1X0+T8px1x6ZiWfbhXL
|
|
||||||
ptc/qCIXjPCgVN3s+Kasii3hHkZxKGZz/ySmBmfDJZjQZtbZzQWpvvX6SD4s7sjo
|
|
||||||
gWbZW3qvQ0bFTGdD1IjKYGaxK6aSrNkAIutiAX4RczJ1QSwb9Z2EIen+ABAvOZS9
|
|
||||||
xv3LiiidWcuOT7WzXEa4QvOslCEkAF+jj6mGYB7NWtly0kj4AEPvI4IoYTi9dohS
|
|
||||||
CA0zd1DTfjRwpAnT5P4sj4mpjLyRBumeeVGpCZhUxfKpFjIB2AnlgxrU+LPq5c9R
|
|
||||||
ZZ9Q5oeLxjRPjpqBeWwgnbjXstQCL9g0U7SsEemsv+zmvG5COhAmG5Wce/65ILlg
|
|
||||||
450H4bcn1ul0xvxz9hat6tqEZry3HcNE/CGDT+tXuhHpqOXkY1/c78C0QbWjWodR
|
|
||||||
tCvlXW00a+7TlEhNr4XBNdqtIQfYS9K9yiVVNfZLPEsN/SA3BGXmrr+du1/E4Ria
|
|
||||||
CkVpmBdJsVu5eMaUj1arsCqI4fwHzljtojJe/pCzZBVkOaSWQEQ+LL4iVnMas68m
|
|
||||||
qyshtNf4KNiM55OQmyTiFHMTIxCtdEcHaR3mUxR7GrIhc/bxyxUUBtMAuUX0Kjs=
|
|
||||||
-----END CERTIFICATE-----
|
|
@ -19,10 +19,6 @@ SHOWLISTMODES="1"
|
|||||||
NOOPEROVERRIDE=""
|
NOOPEROVERRIDE=""
|
||||||
OPEROVERRIDEVERIFY=""
|
OPEROVERRIDEVERIFY=""
|
||||||
GENCERTIFICATE="1"
|
GENCERTIFICATE="1"
|
||||||
|
EXTRAPARA=""
|
||||||
# Use system argon to avoid getting SIGILLed if the build machine has a more recent
|
|
||||||
# CPU than the one running the tests.
|
|
||||||
EXTRAPARA="--with-system-argon2"
|
|
||||||
|
|
||||||
ADVANCED=""
|
ADVANCED=""
|
||||||
|
|
||||||
|
@ -1,27 +1,13 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
import multiprocessing
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
import shutil
|
import shutil
|
||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
import textwrap
|
|
||||||
import time
|
import time
|
||||||
from typing import (
|
from typing import IO, Any, Callable, Dict, List, Optional, Set, Tuple, Type
|
||||||
IO,
|
|
||||||
Any,
|
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
List,
|
|
||||||
MutableMapping,
|
|
||||||
Optional,
|
|
||||||
Set,
|
|
||||||
Tuple,
|
|
||||||
Type,
|
|
||||||
)
|
|
||||||
|
|
||||||
import irctest
|
import irctest
|
||||||
|
|
||||||
@ -70,49 +56,15 @@ class _BaseController:
|
|||||||
supported_sasl_mechanisms: Set[str]
|
supported_sasl_mechanisms: Set[str]
|
||||||
proc: Optional[subprocess.Popen]
|
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):
|
def __init__(self, test_config: TestCaseControllerConfig):
|
||||||
self.test_config = test_config
|
self.test_config = test_config
|
||||||
self.proc = None
|
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:
|
def check_is_alive(self) -> None:
|
||||||
assert self.proc
|
assert self.proc
|
||||||
self.proc.poll()
|
self.proc.poll()
|
||||||
if self.proc.returncode is not None:
|
if self.proc.returncode is not None:
|
||||||
raise ProcessStopped(f"process returned {self.proc.returncode}")
|
raise ProcessStopped()
|
||||||
|
|
||||||
def kill_proc(self) -> None:
|
def kill_proc(self) -> None:
|
||||||
"""Terminates the controlled process, waits for it to exit, and
|
"""Terminates the controlled process, waits for it to exit, and
|
||||||
@ -130,17 +82,12 @@ class _BaseController:
|
|||||||
if self.proc:
|
if self.proc:
|
||||||
self.kill_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):
|
class DirectoryBasedController(_BaseController):
|
||||||
"""Helper for controllers whose software configuration is based on an
|
"""Helper for controllers whose software configuration is based on an
|
||||||
arbitrary directory."""
|
arbitrary directory."""
|
||||||
|
|
||||||
directory: Optional[Path]
|
directory: Optional[str]
|
||||||
|
|
||||||
def __init__(self, test_config: TestCaseControllerConfig):
|
def __init__(self, test_config: TestCaseControllerConfig):
|
||||||
super().__init__(test_config)
|
super().__init__(test_config)
|
||||||
@ -163,21 +110,22 @@ class DirectoryBasedController(_BaseController):
|
|||||||
"""Open a file in the configuration directory."""
|
"""Open a file in the configuration directory."""
|
||||||
assert self.directory
|
assert self.directory
|
||||||
if os.sep in name:
|
if os.sep in name:
|
||||||
dir_ = self.directory / os.path.dirname(name)
|
dir_ = os.path.join(self.directory, os.path.dirname(name))
|
||||||
dir_.mkdir(parents=True, exist_ok=True)
|
if not os.path.isdir(dir_):
|
||||||
assert dir_.is_dir()
|
os.makedirs(dir_)
|
||||||
return (self.directory / name).open(mode)
|
assert os.path.isdir(dir_)
|
||||||
|
return open(os.path.join(self.directory, name), mode)
|
||||||
|
|
||||||
def create_config(self) -> None:
|
def create_config(self) -> None:
|
||||||
if not self.directory:
|
if not self.directory:
|
||||||
self.directory = Path(tempfile.mkdtemp())
|
self.directory = tempfile.mkdtemp()
|
||||||
|
|
||||||
def gen_ssl(self) -> None:
|
def gen_ssl(self) -> None:
|
||||||
assert self.directory
|
assert self.directory
|
||||||
self.csr_path = self.directory / "ssl.csr"
|
self.csr_path = os.path.join(self.directory, "ssl.csr")
|
||||||
self.key_path = self.directory / "ssl.key"
|
self.key_path = os.path.join(self.directory, "ssl.key")
|
||||||
self.pem_path = self.directory / "ssl.pem"
|
self.pem_path = os.path.join(self.directory, "ssl.pem")
|
||||||
self.dh_path = self.directory / "dh.pem"
|
self.dh_path = os.path.join(self.directory, "dh.pem")
|
||||||
subprocess.check_output(
|
subprocess.check_output(
|
||||||
[
|
[
|
||||||
self.openssl_bin,
|
self.openssl_bin,
|
||||||
@ -208,18 +156,10 @@ class DirectoryBasedController(_BaseController):
|
|||||||
],
|
],
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
with self.dh_path.open("w") as fd:
|
subprocess.check_output(
|
||||||
fd.write(
|
[self.openssl_bin, "dhparam", "-out", self.dh_path, "128"],
|
||||||
textwrap.dedent(
|
stderr=subprocess.DEVNULL,
|
||||||
"""
|
)
|
||||||
-----BEGIN DH PARAMETERS-----
|
|
||||||
MIGHAoGBAJICSyQAiLj1fw8b5xELcnpqBQ+wvOyKgim4IetWOgZnRQFkTgOeoRZD
|
|
||||||
HksACRFJL/EqHxDKcy/2Ghwr2axhNxSJ+UOBmraP3WfodV/fCDPnZ+XnI9fjHsIr
|
|
||||||
rjisPMqomjXeiTB1UeAHvLUmCK4yx6lpAJsCYwJjsqkycUfHiy1bAgEC
|
|
||||||
-----END DH PARAMETERS-----
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseClientController(_BaseController):
|
class BaseClientController(_BaseController):
|
||||||
@ -249,9 +189,8 @@ class BaseServerController(_BaseController):
|
|||||||
"""Character used for the 'mute' extban"""
|
"""Character used for the 'mute' extban"""
|
||||||
nickserv = "NickServ"
|
nickserv = "NickServ"
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any):
|
def get_hostname_and_port(self) -> Tuple[str, int]:
|
||||||
super().__init__(*args, **kwargs)
|
return find_hostname_and_port()
|
||||||
self.faketime_enabled = False
|
|
||||||
|
|
||||||
def run(
|
def run(
|
||||||
self,
|
self,
|
||||||
@ -261,7 +200,8 @@ class BaseServerController(_BaseController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str],
|
valid_metadata_keys: Optional[Set[str]],
|
||||||
|
invalid_metadata_keys: Optional[Set[str]],
|
||||||
) -> None:
|
) -> None:
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@ -277,7 +217,6 @@ class BaseServerController(_BaseController):
|
|||||||
raise NotImplementedByController("account registration")
|
raise NotImplementedByController("account registration")
|
||||||
|
|
||||||
def wait_for_port(self) -> None:
|
def wait_for_port(self) -> None:
|
||||||
started_at = time.time()
|
|
||||||
while not self.port_open:
|
while not self.port_open:
|
||||||
self.check_is_alive()
|
self.check_is_alive()
|
||||||
time.sleep(self._port_wait_interval)
|
time.sleep(self._port_wait_interval)
|
||||||
@ -300,16 +239,11 @@ class BaseServerController(_BaseController):
|
|||||||
# ircu2 cuts the connection without a message if registration
|
# ircu2 cuts the connection without a message if registration
|
||||||
# is not complete.
|
# is not complete.
|
||||||
pass
|
pass
|
||||||
except socket.timeout:
|
|
||||||
# irc2 just keeps it open
|
|
||||||
pass
|
|
||||||
|
|
||||||
c.close()
|
c.close()
|
||||||
self.port_open = True
|
self.port_open = True
|
||||||
except ConnectionRefusedError:
|
except Exception:
|
||||||
if time.time() - started_at >= 60:
|
continue
|
||||||
# waited for 60 seconds, giving up
|
|
||||||
raise
|
|
||||||
|
|
||||||
def wait_for_services(self) -> None:
|
def wait_for_services(self) -> None:
|
||||||
assert self.services_controller
|
assert self.services_controller
|
||||||
@ -356,11 +290,10 @@ class BaseServicesController(_BaseController):
|
|||||||
c.sendLine("PONG :" + msg.params[0])
|
c.sendLine("PONG :" + msg.params[0])
|
||||||
c.getMessages()
|
c.getMessages()
|
||||||
|
|
||||||
timeout = time.time() + 3
|
timeout = time.time() + 5
|
||||||
while True:
|
while True:
|
||||||
c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :help")
|
c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :HELP")
|
||||||
|
msgs = self.getNickServResponse(c)
|
||||||
msgs = self.getNickServResponse(c, timeout=1)
|
|
||||||
for msg in msgs:
|
for msg in msgs:
|
||||||
if msg.command == "401":
|
if msg.command == "401":
|
||||||
# NickServ not available yet
|
# NickServ not available yet
|
||||||
@ -386,12 +319,11 @@ class BaseServicesController(_BaseController):
|
|||||||
c.disconnect()
|
c.disconnect()
|
||||||
self.services_up = True
|
self.services_up = True
|
||||||
|
|
||||||
def getNickServResponse(self, client: Any, timeout: int = 0) -> List[Message]:
|
def getNickServResponse(self, client: Any) -> List[Message]:
|
||||||
"""Wrapper aroung getMessages() that waits longer, because NickServ
|
"""Wrapper aroung getMessages() that waits longer, because NickServ
|
||||||
is queried asynchronously."""
|
is queried asynchronously."""
|
||||||
msgs: List[Message] = []
|
msgs: List[Message] = []
|
||||||
start_time = time.time()
|
while not msgs:
|
||||||
while not msgs and (not timeout or start_time + timeout > time.time()):
|
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
msgs = client.getMessages()
|
msgs = client.getMessages()
|
||||||
return msgs
|
return msgs
|
||||||
|
171
irctest/cases.py
171
irctest/cases.py
@ -69,30 +69,6 @@ TController = TypeVar("TController", bound=basecontrollers._BaseController)
|
|||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
def retry(f: TCallable) -> TCallable:
|
|
||||||
"""Retry the function if it raises ConnectionClosed; as a workaround for flaky
|
|
||||||
connection, such as::
|
|
||||||
|
|
||||||
1: connects to server.
|
|
||||||
1 -> S: NICK foo
|
|
||||||
1 -> S: USER username * * :Realname
|
|
||||||
S -> 1: :My.Little.Server NOTICE * :*** Found your hostname (cached)
|
|
||||||
S -> 1: :My.Little.Server NOTICE * :*** Checking Ident
|
|
||||||
S -> 1: :My.Little.Server NOTICE * :*** No Ident response
|
|
||||||
S -> 1: ERROR :Closing Link: cpu-pool.com (Use a different port)
|
|
||||||
"""
|
|
||||||
|
|
||||||
@functools.wraps(f)
|
|
||||||
def newf(*args, **kwargs): # type: ignore
|
|
||||||
try:
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
except ConnectionClosed:
|
|
||||||
time.sleep(1)
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
return newf # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
class ChannelJoinException(Exception):
|
class ChannelJoinException(Exception):
|
||||||
def __init__(self, code: str, params: List[str]):
|
def __init__(self, code: str, params: List[str]):
|
||||||
super().__init__(f"Failed to join channel ({code}): {params}")
|
super().__init__(f"Failed to join channel ({code}): {params}")
|
||||||
@ -173,7 +149,7 @@ class _IrcTestCase(Generic[TController]):
|
|||||||
) -> Optional[str]:
|
) -> Optional[str]:
|
||||||
"""Returns an error message if the message doesn't match the given arguments,
|
"""Returns an error message if the message doesn't match the given arguments,
|
||||||
or None if it matches."""
|
or None if it matches."""
|
||||||
for key, value in kwargs.items():
|
for (key, value) in kwargs.items():
|
||||||
if getattr(msg, key) != value:
|
if getattr(msg, key) != value:
|
||||||
fail_msg = (
|
fail_msg = (
|
||||||
fail_msg or "expected {param} to be {expects}, got {got}: {msg}"
|
fail_msg or "expected {param} to be {expects}, got {got}: {msg}"
|
||||||
@ -186,7 +162,7 @@ class _IrcTestCase(Generic[TController]):
|
|||||||
msg=msg,
|
msg=msg,
|
||||||
)
|
)
|
||||||
|
|
||||||
if prefix is not None and not patma.match_string(msg.prefix, prefix):
|
if prefix and not patma.match_string(msg.prefix, prefix):
|
||||||
fail_msg = (
|
fail_msg = (
|
||||||
fail_msg or "expected prefix to match {expects}, got {got}: {msg}"
|
fail_msg or "expected prefix to match {expects}, got {got}: {msg}"
|
||||||
)
|
)
|
||||||
@ -194,7 +170,7 @@ class _IrcTestCase(Generic[TController]):
|
|||||||
*extra_format, got=msg.prefix, expects=prefix, msg=msg
|
*extra_format, got=msg.prefix, expects=prefix, msg=msg
|
||||||
)
|
)
|
||||||
|
|
||||||
if params is not None and not patma.match_list(list(msg.params), params):
|
if params and not patma.match_list(list(msg.params), params):
|
||||||
fail_msg = (
|
fail_msg = (
|
||||||
fail_msg or "expected params to match {expects}, got {got}: {msg}"
|
fail_msg or "expected params to match {expects}, got {got}: {msg}"
|
||||||
)
|
)
|
||||||
@ -202,11 +178,11 @@ class _IrcTestCase(Generic[TController]):
|
|||||||
*extra_format, got=msg.params, expects=params, msg=msg
|
*extra_format, got=msg.params, expects=params, msg=msg
|
||||||
)
|
)
|
||||||
|
|
||||||
if tags is not None and not patma.match_dict(msg.tags, tags):
|
if tags and not patma.match_dict(msg.tags, tags):
|
||||||
fail_msg = fail_msg or "expected tags to match {expects}, got {got}: {msg}"
|
fail_msg = fail_msg or "expected tags to match {expects}, got {got}: {msg}"
|
||||||
return fail_msg.format(*extra_format, got=msg.tags, expects=tags, msg=msg)
|
return fail_msg.format(*extra_format, got=msg.tags, expects=tags, msg=msg)
|
||||||
|
|
||||||
if nick is not None:
|
if nick:
|
||||||
got_nick = msg.prefix.split("!")[0] if msg.prefix else None
|
got_nick = msg.prefix.split("!")[0] if msg.prefix else None
|
||||||
if nick != got_nick:
|
if nick != got_nick:
|
||||||
fail_msg = (
|
fail_msg = (
|
||||||
@ -351,8 +327,8 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
|
|||||||
nick: Optional[str] = None
|
nick: Optional[str] = None
|
||||||
user: Optional[List[str]] = None
|
user: Optional[List[str]] = None
|
||||||
server: socket.socket
|
server: socket.socket
|
||||||
protocol_version: Optional[str]
|
protocol_version = Optional[str]
|
||||||
acked_capabilities: Optional[Set[str]]
|
acked_capabilities = Optional[Set[str]]
|
||||||
|
|
||||||
__new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise
|
__new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise
|
||||||
|
|
||||||
@ -448,9 +424,7 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
|
|||||||
print("{:.3f} S: {}".format(time.time(), line.strip()))
|
print("{:.3f} S: {}".format(time.time(), line.strip()))
|
||||||
|
|
||||||
def readCapLs(
|
def readCapLs(
|
||||||
self,
|
self, auth: Optional[Authentication] = None, tls_config: tls.TlsConfig = None
|
||||||
auth: Optional[Authentication] = None,
|
|
||||||
tls_config: Optional[tls.TlsConfig] = None,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
(hostname, port) = self.server.getsockname()
|
(hostname, port) = self.server.getsockname()
|
||||||
self.controller.run(
|
self.controller.run(
|
||||||
@ -460,9 +434,9 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]):
|
|||||||
m = self.getMessage()
|
m = self.getMessage()
|
||||||
self.assertEqual(m.command, "CAP", "First message is not CAP LS.")
|
self.assertEqual(m.command, "CAP", "First message is not CAP LS.")
|
||||||
if m.params == ["LS"]:
|
if m.params == ["LS"]:
|
||||||
self.protocol_version = "301"
|
self.protocol_version = 301
|
||||||
elif m.params == ["LS", "302"]:
|
elif m.params == ["LS", "302"]:
|
||||||
self.protocol_version = "302"
|
self.protocol_version = 302
|
||||||
elif m.params == ["END"]:
|
elif m.params == ["END"]:
|
||||||
self.protocol_version = None
|
self.protocol_version = None
|
||||||
else:
|
else:
|
||||||
@ -529,15 +503,11 @@ class BaseServerTestCase(
|
|||||||
|
|
||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
ssl = False
|
ssl = False
|
||||||
|
valid_metadata_keys: Set[str] = set()
|
||||||
|
invalid_metadata_keys: Set[str] = set()
|
||||||
server_support: Optional[Dict[str, Optional[str]]]
|
server_support: Optional[Dict[str, Optional[str]]]
|
||||||
run_services = False
|
run_services = False
|
||||||
|
|
||||||
faketime: Optional[str] = None
|
|
||||||
"""If not None and the controller supports it and libfaketime is available,
|
|
||||||
runs the server using faketime and this value set as the $FAKETIME env variable.
|
|
||||||
Tests must check ``self.controller.faketime_enabled`` is True before
|
|
||||||
relying on this."""
|
|
||||||
|
|
||||||
__new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise
|
__new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise
|
||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
@ -548,9 +518,10 @@ class BaseServerTestCase(
|
|||||||
self.hostname,
|
self.hostname,
|
||||||
self.port,
|
self.port,
|
||||||
password=self.password,
|
password=self.password,
|
||||||
|
valid_metadata_keys=self.valid_metadata_keys,
|
||||||
|
invalid_metadata_keys=self.invalid_metadata_keys,
|
||||||
ssl=self.ssl,
|
ssl=self.ssl,
|
||||||
run_services=self.run_services,
|
run_services=self.run_services,
|
||||||
faketime=self.faketime,
|
|
||||||
)
|
)
|
||||||
self.clients: Dict[TClientName, client_mock.ClientMock] = {}
|
self.clients: Dict[TClientName, client_mock.ClientMock] = {}
|
||||||
|
|
||||||
@ -568,10 +539,13 @@ class BaseServerTestCase(
|
|||||||
if self.run_services:
|
if self.run_services:
|
||||||
self.controller.wait_for_services()
|
self.controller.wait_for_services()
|
||||||
if not name:
|
if not name:
|
||||||
used_ids: List[int] = [
|
new_name: int = (
|
||||||
int(name) for name in self.clients if isinstance(name, (int, str))
|
max(
|
||||||
]
|
[int(name) for name in self.clients if isinstance(name, (int, str))]
|
||||||
new_name = max(used_ids + [0]) + 1
|
+ [0]
|
||||||
|
)
|
||||||
|
+ 1
|
||||||
|
)
|
||||||
name = cast(TClientName, new_name)
|
name = cast(TClientName, new_name)
|
||||||
show_io = show_io if show_io is not None else self.show_io
|
show_io = show_io if show_io is not None else self.show_io
|
||||||
self.clients[name] = client_mock.ClientMock(name=name, show_io=show_io)
|
self.clients[name] = client_mock.ClientMock(name=name, show_io=show_io)
|
||||||
@ -673,21 +647,10 @@ class BaseServerTestCase(
|
|||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
def authenticateClient(
|
|
||||||
self, client: TClientName, account: str, password: str
|
|
||||||
) -> None:
|
|
||||||
self.sendLine(client, "AUTHENTICATE PLAIN")
|
|
||||||
m = self.getRegistrationMessage(client)
|
|
||||||
self.assertMessageMatch(m, command="AUTHENTICATE", params=["+"])
|
|
||||||
self.sendLine(client, sasl_plain_blob(account, password))
|
|
||||||
m = self.getRegistrationMessage(client)
|
|
||||||
self.assertIn(m.command, ["900", "903"], str(m))
|
|
||||||
|
|
||||||
@retry
|
|
||||||
def connectClient(
|
def connectClient(
|
||||||
self,
|
self,
|
||||||
nick: str,
|
nick: str,
|
||||||
name: Optional[TClientName] = None,
|
name: TClientName = None,
|
||||||
capabilities: Optional[List[str]] = None,
|
capabilities: Optional[List[str]] = None,
|
||||||
skip_if_cap_nak: bool = False,
|
skip_if_cap_nak: bool = False,
|
||||||
show_io: Optional[bool] = None,
|
show_io: Optional[bool] = None,
|
||||||
@ -702,12 +665,17 @@ class BaseServerTestCase(
|
|||||||
client = self.addClient(name, show_io=show_io)
|
client = self.addClient(name, show_io=show_io)
|
||||||
if capabilities:
|
if capabilities:
|
||||||
self.sendLine(client, "CAP LS 302")
|
self.sendLine(client, "CAP LS 302")
|
||||||
self.getCapLs(client)
|
m = self.getRegistrationMessage(client)
|
||||||
self.requestCapabilities(client, capabilities, skip_if_cap_nak)
|
self.requestCapabilities(client, capabilities, skip_if_cap_nak)
|
||||||
if password is not None:
|
if password is not None:
|
||||||
if "sasl" not in (capabilities or ()):
|
if "sasl" not in (capabilities or ()):
|
||||||
raise ValueError("Used 'password' option without sasl capbilitiy")
|
raise ValueError("Used 'password' option without sasl capbilitiy")
|
||||||
self.authenticateClient(client, account or nick, password)
|
self.sendLine(client, "AUTHENTICATE PLAIN")
|
||||||
|
m = self.getRegistrationMessage(client)
|
||||||
|
self.assertMessageMatch(m, command="AUTHENTICATE", params=["+"])
|
||||||
|
self.sendLine(client, sasl_plain_blob(account or nick, password))
|
||||||
|
m = self.getRegistrationMessage(client)
|
||||||
|
self.assertIn(m.command, ["900", "903"], str(m))
|
||||||
|
|
||||||
self.sendLine(client, "NICK {}".format(nick))
|
self.sendLine(client, "NICK {}".format(nick))
|
||||||
self.sendLine(client, "USER %s * * :Realname" % (ident,))
|
self.sendLine(client, "USER %s * * :Realname" % (ident,))
|
||||||
@ -732,12 +700,6 @@ class BaseServerTestCase(
|
|||||||
self.server_support[param] = None
|
self.server_support[param] = None
|
||||||
welcome.append(m)
|
welcome.append(m)
|
||||||
|
|
||||||
self.targmax: Dict[str, Optional[str]] = dict( # type: ignore[assignment]
|
|
||||||
item.split(":", 1)
|
|
||||||
for item in (self.server_support.get("TARGMAX") or "").split(",")
|
|
||||||
if item
|
|
||||||
)
|
|
||||||
|
|
||||||
return welcome
|
return welcome
|
||||||
|
|
||||||
def joinClient(self, client: TClientName, channel: str) -> None:
|
def joinClient(self, client: TClientName, channel: str) -> None:
|
||||||
@ -768,55 +730,50 @@ class BaseServerTestCase(
|
|||||||
raise ChannelJoinException(msg.command, msg.params)
|
raise ChannelJoinException(msg.command, msg.params)
|
||||||
|
|
||||||
|
|
||||||
_TSelf = TypeVar("_TSelf", bound="_IrcTestCase")
|
_TSelf = TypeVar("_TSelf", bound="OptionalityHelper")
|
||||||
_TReturn = TypeVar("_TReturn")
|
_TReturn = TypeVar("_TReturn")
|
||||||
|
|
||||||
|
|
||||||
def skipUnlessHasMechanism(
|
class OptionalityHelper(Generic[TController]):
|
||||||
mech: str,
|
controller: TController
|
||||||
) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]:
|
|
||||||
# Just a function returning a function that takes functions and
|
def checkSaslSupport(self) -> None:
|
||||||
# returns functions, nothing to see here.
|
if self.controller.supported_sasl_mechanisms:
|
||||||
# If Python didn't have such an awful syntax for callables, it would be:
|
return
|
||||||
# str -> ((TSelf -> TReturn) -> (TSelf -> TReturn))
|
raise runner.NotImplementedByController("SASL")
|
||||||
def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]:
|
|
||||||
|
def checkMechanismSupport(self, mechanism: str) -> None:
|
||||||
|
if mechanism in self.controller.supported_sasl_mechanisms:
|
||||||
|
return
|
||||||
|
raise runner.OptionalSaslMechanismNotSupported(mechanism)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def skipUnlessHasMechanism(
|
||||||
|
mech: str,
|
||||||
|
) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]:
|
||||||
|
# Just a function returning a function that takes functions and
|
||||||
|
# returns functions, nothing to see here.
|
||||||
|
# If Python didn't have such an awful syntax for callables, it would be:
|
||||||
|
# str -> ((TSelf -> TReturn) -> (TSelf -> TReturn))
|
||||||
|
def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]:
|
||||||
|
@functools.wraps(f)
|
||||||
|
def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn:
|
||||||
|
self.checkMechanismSupport(mech)
|
||||||
|
return f(self, *args, **kwargs)
|
||||||
|
|
||||||
|
return newf
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def skipUnlessHasSasl(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]:
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn:
|
def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn:
|
||||||
if mech not in self.controller.supported_sasl_mechanisms:
|
self.checkSaslSupport()
|
||||||
raise runner.OptionalSaslMechanismNotSupported(mech)
|
|
||||||
return f(self, *args, **kwargs)
|
return f(self, *args, **kwargs)
|
||||||
|
|
||||||
return newf
|
return newf
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def xfailIf(
|
|
||||||
condition: Callable[..., bool], reason: str
|
|
||||||
) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]:
|
|
||||||
# Works about the same as skipUnlessHasMechanism
|
|
||||||
def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]:
|
|
||||||
@functools.wraps(f)
|
|
||||||
def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn:
|
|
||||||
if condition(self):
|
|
||||||
try:
|
|
||||||
return f(self, *args, **kwargs)
|
|
||||||
except Exception:
|
|
||||||
pytest.xfail(reason)
|
|
||||||
assert False # make mypy happy
|
|
||||||
else:
|
|
||||||
return f(self, *args, **kwargs)
|
|
||||||
|
|
||||||
return newf
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def xfailIfSoftware(
|
|
||||||
names: List[str], reason: str
|
|
||||||
) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]:
|
|
||||||
return xfailIf(lambda testcase: testcase.controller.software_name in names, reason)
|
|
||||||
|
|
||||||
|
|
||||||
def mark_services(cls: TClass) -> TClass:
|
def mark_services(cls: TClass) -> TClass:
|
||||||
cls.run_services = True
|
cls.run_services = True
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
"""Format of ``CAP LS`` sent by IRCv3 clients."""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.irc_utils.message_parser import Message
|
from irctest.irc_utils.message_parser import Message
|
||||||
|
|
||||||
|
@ -1,8 +1,3 @@
|
|||||||
"""SASL authentication from clients, for all known mechanisms.
|
|
||||||
|
|
||||||
For now, only `SASLv3.1 <https://ircv3.net/specs/extensions/sasl-3.1>`_
|
|
||||||
is tested, not `SASLv3.2 <https://ircv3.net/specs/extensions/sasl-3.2>`_."""
|
|
||||||
|
|
||||||
import base64
|
import base64
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -39,8 +34,8 @@ class IdentityHash:
|
|||||||
return self._data
|
return self._data
|
||||||
|
|
||||||
|
|
||||||
class SaslTestCase(cases.BaseClientTestCase):
|
class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper):
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
def testPlain(self):
|
def testPlain(self):
|
||||||
"""Test PLAIN authentication with correct username/password."""
|
"""Test PLAIN authentication with correct username/password."""
|
||||||
auth = authentication.Authentication(
|
auth = authentication.Authentication(
|
||||||
@ -60,8 +55,7 @@ class SaslTestCase(cases.BaseClientTestCase):
|
|||||||
m = self.negotiateCapabilities(["sasl"], False)
|
m = self.negotiateCapabilities(["sasl"], False)
|
||||||
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
|
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
|
||||||
|
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
@cases.xfailIfSoftware(["Sopel"], "Sopel requests SASL PLAIN even if not available")
|
|
||||||
def testPlainNotAvailable(self):
|
def testPlainNotAvailable(self):
|
||||||
"""`sasl=EXTERNAL` is advertized, whereas the client is configured
|
"""`sasl=EXTERNAL` is advertized, whereas the client is configured
|
||||||
to use PLAIN.
|
to use PLAIN.
|
||||||
@ -90,9 +84,8 @@ class SaslTestCase(cases.BaseClientTestCase):
|
|||||||
m = self.getMessage()
|
m = self.getMessage()
|
||||||
self.assertMessageMatch(m, command="CAP")
|
self.assertMessageMatch(m, command="CAP")
|
||||||
|
|
||||||
@pytest.mark.parametrize("pattern", ["barbaz", "éèà"])
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
def testPlainLarge(self):
|
||||||
def testPlainLarge(self, pattern):
|
|
||||||
"""Test the client splits large AUTHENTICATE messages whose payload
|
"""Test the client splits large AUTHENTICATE messages whose payload
|
||||||
is not a multiple of 400.
|
is not a multiple of 400.
|
||||||
<http://ircv3.net/specs/extensions/sasl-3.1.html#the-authenticate-command>
|
<http://ircv3.net/specs/extensions/sasl-3.1.html#the-authenticate-command>
|
||||||
@ -101,10 +94,10 @@ class SaslTestCase(cases.BaseClientTestCase):
|
|||||||
auth = authentication.Authentication(
|
auth = authentication.Authentication(
|
||||||
mechanisms=[authentication.Mechanisms.plain],
|
mechanisms=[authentication.Mechanisms.plain],
|
||||||
username="foo",
|
username="foo",
|
||||||
password=pattern * 100,
|
password="bar" * 200,
|
||||||
)
|
)
|
||||||
authstring = base64.b64encode(
|
authstring = base64.b64encode(
|
||||||
b"\x00".join([b"foo", b"foo", pattern.encode() * 100])
|
b"\x00".join([b"foo", b"foo", b"bar" * 200])
|
||||||
).decode()
|
).decode()
|
||||||
m = self.negotiateCapabilities(["sasl"], auth=auth)
|
m = self.negotiateCapabilities(["sasl"], auth=auth)
|
||||||
self.assertEqual(m, Message({}, None, "AUTHENTICATE", ["PLAIN"]))
|
self.assertEqual(m, Message({}, None, "AUTHENTICATE", ["PLAIN"]))
|
||||||
@ -120,9 +113,8 @@ class SaslTestCase(cases.BaseClientTestCase):
|
|||||||
m = self.negotiateCapabilities(["sasl"], False)
|
m = self.negotiateCapabilities(["sasl"], False)
|
||||||
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
|
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
|
||||||
|
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
@pytest.mark.parametrize("pattern", ["quux", "éè"])
|
def testPlainLargeMultiple(self):
|
||||||
def testPlainLargeMultiple(self, pattern):
|
|
||||||
"""Test the client splits large AUTHENTICATE messages whose payload
|
"""Test the client splits large AUTHENTICATE messages whose payload
|
||||||
is a multiple of 400.
|
is a multiple of 400.
|
||||||
<http://ircv3.net/specs/extensions/sasl-3.1.html#the-authenticate-command>
|
<http://ircv3.net/specs/extensions/sasl-3.1.html#the-authenticate-command>
|
||||||
@ -131,10 +123,10 @@ class SaslTestCase(cases.BaseClientTestCase):
|
|||||||
auth = authentication.Authentication(
|
auth = authentication.Authentication(
|
||||||
mechanisms=[authentication.Mechanisms.plain],
|
mechanisms=[authentication.Mechanisms.plain],
|
||||||
username="foo",
|
username="foo",
|
||||||
password=pattern * 148,
|
password="quux" * 148,
|
||||||
)
|
)
|
||||||
authstring = base64.b64encode(
|
authstring = base64.b64encode(
|
||||||
b"\x00".join([b"foo", b"foo", pattern.encode() * 148])
|
b"\x00".join([b"foo", b"foo", b"quux" * 148])
|
||||||
).decode()
|
).decode()
|
||||||
m = self.negotiateCapabilities(["sasl"], auth=auth)
|
m = self.negotiateCapabilities(["sasl"], auth=auth)
|
||||||
self.assertEqual(m, Message({}, None, "AUTHENTICATE", ["PLAIN"]))
|
self.assertEqual(m, Message({}, None, "AUTHENTICATE", ["PLAIN"]))
|
||||||
@ -151,7 +143,7 @@ class SaslTestCase(cases.BaseClientTestCase):
|
|||||||
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
|
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
|
||||||
|
|
||||||
@pytest.mark.skipif(ecdsa is None, reason="python3-ecdsa is not available")
|
@pytest.mark.skipif(ecdsa is None, reason="python3-ecdsa is not available")
|
||||||
@cases.skipUnlessHasMechanism("ECDSA-NIST256P-CHALLENGE")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("ECDSA-NIST256P-CHALLENGE")
|
||||||
def testEcdsa(self):
|
def testEcdsa(self):
|
||||||
"""Test ECDSA authentication."""
|
"""Test ECDSA authentication."""
|
||||||
auth = authentication.Authentication(
|
auth = authentication.Authentication(
|
||||||
@ -185,7 +177,7 @@ class SaslTestCase(cases.BaseClientTestCase):
|
|||||||
m = self.negotiateCapabilities(["sasl"], False)
|
m = self.negotiateCapabilities(["sasl"], False)
|
||||||
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
|
self.assertEqual(m, Message({}, None, "CAP", ["END"]))
|
||||||
|
|
||||||
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256")
|
||||||
def testScram(self):
|
def testScram(self):
|
||||||
"""Test SCRAM-SHA-256 authentication."""
|
"""Test SCRAM-SHA-256 authentication."""
|
||||||
auth = authentication.Authentication(
|
auth = authentication.Authentication(
|
||||||
@ -227,8 +219,8 @@ class SaslTestCase(cases.BaseClientTestCase):
|
|||||||
self.assertEqual(m.command, "AUTHENTICATE", m)
|
self.assertEqual(m.command, "AUTHENTICATE", m)
|
||||||
self.assertEqual(m.params, ["+"], m)
|
self.assertEqual(m.params, ["+"], m)
|
||||||
|
|
||||||
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256")
|
||||||
def testScramBadPassword(self, server_fakes_success=False, fake_response=None):
|
def testScramBadPassword(self):
|
||||||
"""Test SCRAM-SHA-256 authentication with a bad password."""
|
"""Test SCRAM-SHA-256 authentication with a bad password."""
|
||||||
auth = authentication.Authentication(
|
auth = authentication.Authentication(
|
||||||
mechanisms=[authentication.Mechanisms.scram_sha_256],
|
mechanisms=[authentication.Mechanisms.scram_sha_256],
|
||||||
@ -261,39 +253,9 @@ class SaslTestCase(cases.BaseClientTestCase):
|
|||||||
with self.assertRaises(scram.NotAuthorizedException):
|
with self.assertRaises(scram.NotAuthorizedException):
|
||||||
authenticator.response(msg)
|
authenticator.response(msg)
|
||||||
|
|
||||||
if server_fakes_success:
|
|
||||||
self.sendLine(f"AUTHENTICATE :{fake_response}")
|
|
||||||
|
|
||||||
m = self.getMessage()
|
class Irc302SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper):
|
||||||
while m.command == "PING":
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
self.sendLine(f"PONG server. {m.params[-1]}")
|
|
||||||
m = self.getMessage()
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="AUTHENTICATE",
|
|
||||||
params=["*"],
|
|
||||||
fail_msg="Client did not abort: {msg}",
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"fake_response",
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
"AAAA",
|
|
||||||
"dj1ubU1mM1FIV2NKUWk5cE1ndHFLU0tQclZueUk2c3FOTzZJN3BFLzBveUdjPQ==",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def testScramMaliciousServer(self, fake_response):
|
|
||||||
"""Test SCRAM-SHA-256 authentication to a server which pretends to know
|
|
||||||
the password"""
|
|
||||||
self.testScramBadPassword(
|
|
||||||
server_fakes_success=True, fake_response=fake_response
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Irc302SaslTestCase(cases.BaseClientTestCase):
|
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
|
||||||
def testPlainNotAvailable(self):
|
def testPlainNotAvailable(self):
|
||||||
"""Test the client does not try to authenticate using a mechanism the
|
"""Test the client does not try to authenticate using a mechanism the
|
||||||
server does not advertise.
|
server does not advertise.
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
"""Clients should validate certificates; either with a CA or fingerprints."""
|
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from irctest import cases, runner, tls
|
from irctest import cases, runner, tls
|
||||||
from irctest.exceptions import ConnectionClosed
|
from irctest.exceptions import ConnectionClosed
|
||||||
from irctest.patma import ANYSTR
|
from irctest.patma import ANYSTR
|
||||||
@ -140,7 +136,7 @@ class TlsTestCase(cases.BaseClientTestCase):
|
|||||||
self.getMessage()
|
self.getMessage()
|
||||||
|
|
||||||
|
|
||||||
class StsTestCase(cases.BaseClientTestCase):
|
class StsTestCase(cases.BaseClientTestCase, cases.OptionalityHelper):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.insecure_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
self.insecure_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||||
@ -152,8 +148,7 @@ class StsTestCase(cases.BaseClientTestCase):
|
|||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
@cases.mark_capabilities("sts")
|
@cases.mark_capabilities("sts")
|
||||||
@pytest.mark.parametrize("portOnSecure", [False, True])
|
def testSts(self):
|
||||||
def testSts(self, portOnSecure):
|
|
||||||
if not self.controller.supports_sts:
|
if not self.controller.supports_sts:
|
||||||
raise runner.CapabilityNotSupported("sts")
|
raise runner.CapabilityNotSupported("sts")
|
||||||
tls_config = tls.TlsConfig(
|
tls_config = tls.TlsConfig(
|
||||||
@ -181,12 +176,10 @@ class StsTestCase(cases.BaseClientTestCase):
|
|||||||
# and reconnect securely on the stated port."
|
# and reconnect securely on the stated port."
|
||||||
self.acceptClient(tls_cert=GOOD_CERT, tls_key=GOOD_KEY)
|
self.acceptClient(tls_cert=GOOD_CERT, tls_key=GOOD_KEY)
|
||||||
|
|
||||||
# Send the STS policy, over secure connection this time.
|
# Send the STS policy, over secure connection this time
|
||||||
if portOnSecure:
|
self.sendLine(
|
||||||
# Should be ignored
|
"CAP * LS :sts=duration=10,port={}".format(self.server.getsockname()[1])
|
||||||
self.sendLine("CAP * LS :sts=duration=10,port=12345")
|
)
|
||||||
else:
|
|
||||||
self.sendLine("CAP * LS :sts=duration=10")
|
|
||||||
|
|
||||||
# Make the client reconnect. It should reconnect to the secure server.
|
# Make the client reconnect. It should reconnect to the secure server.
|
||||||
self.sendLine("ERROR :closing link")
|
self.sendLine("ERROR :closing link")
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from pathlib import Path
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Type
|
from typing import Type
|
||||||
@ -73,8 +73,6 @@ module {{ name = "ns_cert" }}
|
|||||||
class AnopeController(BaseServicesController, DirectoryBasedController):
|
class AnopeController(BaseServicesController, DirectoryBasedController):
|
||||||
"""Collaborator for server controllers that rely on Anope"""
|
"""Collaborator for server controllers that rely on Anope"""
|
||||||
|
|
||||||
software_name = "Anope"
|
|
||||||
|
|
||||||
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
|
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
|
||||||
self.create_config()
|
self.create_config()
|
||||||
|
|
||||||
@ -101,11 +99,14 @@ class AnopeController(BaseServicesController, DirectoryBasedController):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
assert self.directory
|
assert self.directory
|
||||||
services_path = shutil.which("services")
|
|
||||||
assert services_path
|
|
||||||
|
|
||||||
# Config and code need to be in the same directory, *obviously*
|
# Config and code need to be in the same directory, *obviously*
|
||||||
(self.directory / "lib").symlink_to(Path(services_path).parent.parent / "lib")
|
os.symlink(
|
||||||
|
os.path.join(
|
||||||
|
os.path.dirname(shutil.which("services")), "..", "lib" # type: ignore
|
||||||
|
),
|
||||||
|
os.path.join(self.directory, "lib"),
|
||||||
|
)
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
@ -55,8 +56,6 @@ saslserv {{
|
|||||||
class AthemeController(BaseServicesController, DirectoryBasedController):
|
class AthemeController(BaseServicesController, DirectoryBasedController):
|
||||||
"""Mixin for server controllers that rely on Atheme"""
|
"""Mixin for server controllers that rely on Atheme"""
|
||||||
|
|
||||||
software_name = "Atheme"
|
|
||||||
|
|
||||||
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
|
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
|
||||||
self.create_config()
|
self.create_config()
|
||||||
|
|
||||||
@ -80,11 +79,11 @@ class AthemeController(BaseServicesController, DirectoryBasedController):
|
|||||||
"atheme-services",
|
"atheme-services",
|
||||||
"-n", # don't fork
|
"-n", # don't fork
|
||||||
"-c",
|
"-c",
|
||||||
self.directory / "services.conf",
|
os.path.join(self.directory, "services.conf"),
|
||||||
"-l",
|
"-l",
|
||||||
f"/tmp/services-{server_port}.log",
|
f"/tmp/services-{server_port}.log",
|
||||||
"-p",
|
"-p",
|
||||||
self.directory / "services.pid",
|
os.path.join(self.directory, "services.pid"),
|
||||||
"-D",
|
"-D",
|
||||||
self.directory,
|
self.directory,
|
||||||
],
|
],
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
from pathlib import Path
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Set, Type
|
from typing import Optional, Set, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
from irctest.basecontrollers import (
|
||||||
|
BaseServerController,
|
||||||
|
DirectoryBasedController,
|
||||||
|
NotImplementedByController,
|
||||||
|
)
|
||||||
|
from irctest.irc_utils.junkdrawer import find_hostname_and_port
|
||||||
|
|
||||||
TEMPLATE_CONFIG = """
|
TEMPLATE_CONFIG = """
|
||||||
global {{
|
global {{
|
||||||
@ -75,19 +80,6 @@ oper {{
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def initialize_entropy(directory: Path) -> None:
|
|
||||||
# https://github.com/DALnet/bahamut/blob/7fc039d403f66a954225c5dc4ad1fe683aedd794/include/dh.h#L35-L38
|
|
||||||
nb_rand_bytes = 512 // 8
|
|
||||||
# https://github.com/DALnet/bahamut/blob/7fc039d403f66a954225c5dc4ad1fe683aedd794/src/dh.c#L186
|
|
||||||
entropy_file_size = nb_rand_bytes * 4
|
|
||||||
|
|
||||||
# Not actually random; but we don't care.
|
|
||||||
entropy = b"\x00" * entropy_file_size
|
|
||||||
|
|
||||||
with (directory / ".ircd.entropy").open("wb") as fd:
|
|
||||||
fd.write(entropy)
|
|
||||||
|
|
||||||
|
|
||||||
class BahamutController(BaseServerController, DirectoryBasedController):
|
class BahamutController(BaseServerController, DirectoryBasedController):
|
||||||
software_name = "Bahamut"
|
software_name = "Bahamut"
|
||||||
supported_sasl_mechanisms: Set[str] = set()
|
supported_sasl_mechanisms: Set[str] = set()
|
||||||
@ -107,14 +99,20 @@ class BahamutController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str],
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
restricted_metadata_keys: Optional[Set[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if valid_metadata_keys or invalid_metadata_keys:
|
||||||
|
raise NotImplementedByController(
|
||||||
|
"Defining valid and invalid METADATA keys."
|
||||||
|
)
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.create_config()
|
self.create_config()
|
||||||
(unused_hostname, unused_port) = self.get_hostname_and_port()
|
(unused_hostname, unused_port) = find_hostname_and_port()
|
||||||
(services_hostname, services_port) = self.get_hostname_and_port()
|
(services_hostname, services_port) = find_hostname_and_port()
|
||||||
|
|
||||||
password_field = "passwd {};".format(password) if password else ""
|
password_field = "passwd {};".format(password) if password else ""
|
||||||
|
|
||||||
@ -122,14 +120,9 @@ class BahamutController(BaseServerController, DirectoryBasedController):
|
|||||||
|
|
||||||
assert self.directory
|
assert self.directory
|
||||||
|
|
||||||
# Bahamut reads some bytes from /dev/urandom on startup, which causes
|
|
||||||
# GitHub Actions to sometimes freeze and timeout.
|
|
||||||
# This initializes the entropy file so Bahamut does not need to do it itself.
|
|
||||||
initialize_entropy(self.directory)
|
|
||||||
|
|
||||||
# they are hardcoded... thankfully Bahamut reads them from the CWD.
|
# they are hardcoded... thankfully Bahamut reads them from the CWD.
|
||||||
shutil.copy(self.pem_path, self.directory / "ircd.crt")
|
shutil.copy(self.pem_path, os.path.join(self.directory, "ircd.crt"))
|
||||||
shutil.copy(self.key_path, self.directory / "ircd.key")
|
shutil.copy(self.key_path, os.path.join(self.directory, "ircd.key"))
|
||||||
|
|
||||||
with self.open_file("server.conf") as fd:
|
with self.open_file("server.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
@ -143,21 +136,15 @@ class BahamutController(BaseServerController, DirectoryBasedController):
|
|||||||
# pem_path=self.pem_path,
|
# pem_path=self.pem_path,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if faketime and shutil.which("faketime"):
|
|
||||||
faketime_cmd = ["faketime", "-f", faketime]
|
|
||||||
self.faketime_enabled = True
|
|
||||||
else:
|
|
||||||
faketime_cmd = []
|
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
# "strace", "-f", "-e", "file",
|
||||||
"ircd",
|
"ircd",
|
||||||
"-t", # don't fork
|
"-t", # don't fork
|
||||||
"-f",
|
"-f",
|
||||||
self.directory / "server.conf",
|
os.path.join(self.directory, "server.conf"),
|
||||||
],
|
],
|
||||||
|
# stdout=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
if run_services:
|
if run_services:
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import shutil
|
import os
|
||||||
import subprocess
|
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 = """
|
TEMPLATE_SSL_CONFIG = """
|
||||||
ssl_private_key = "{key_path}";
|
ssl_private_key = "{key_path}";
|
||||||
@ -36,13 +41,18 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str],
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if valid_metadata_keys or invalid_metadata_keys:
|
||||||
|
raise NotImplementedByController(
|
||||||
|
"Defining valid and invalid METADATA keys."
|
||||||
|
)
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.create_config()
|
self.create_config()
|
||||||
(services_hostname, services_port) = self.get_hostname_and_port()
|
(services_hostname, services_port) = find_hostname_and_port()
|
||||||
password_field = 'password = "{}";'.format(password) if password else ""
|
password_field = 'password = "{}";'.format(password) if password else ""
|
||||||
if ssl:
|
if ssl:
|
||||||
self.gen_ssl()
|
self.gen_ssl()
|
||||||
@ -63,22 +73,14 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert self.directory
|
assert self.directory
|
||||||
|
|
||||||
if faketime and shutil.which("faketime"):
|
|
||||||
faketime_cmd = ["faketime", "-f", faketime]
|
|
||||||
self.faketime_enabled = True
|
|
||||||
else:
|
|
||||||
faketime_cmd = []
|
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
|
||||||
self.binary_name,
|
self.binary_name,
|
||||||
"-foreground",
|
"-foreground",
|
||||||
"-configfile",
|
"-configfile",
|
||||||
self.directory / "server.conf",
|
os.path.join(self.directory, "server.conf"),
|
||||||
"-pidfile",
|
"-pidfile",
|
||||||
self.directory / "server.pid",
|
os.path.join(self.directory, "server.pid"),
|
||||||
],
|
],
|
||||||
# stderr=subprocess.DEVNULL,
|
# stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
@ -12,9 +12,6 @@ serverinfo {{
|
|||||||
|
|
||||||
general {{
|
general {{
|
||||||
throttle_count = 100; # We need to connect lots of clients quickly
|
throttle_count = 100; # We need to connect lots of clients quickly
|
||||||
# disable throttling for LIST and similar:
|
|
||||||
pace_wait_simple = 0 second;
|
|
||||||
pace_wait = 0 second;
|
|
||||||
sasl_service = "SaslServ";
|
sasl_service = "SaslServ";
|
||||||
}};
|
}};
|
||||||
|
|
||||||
|
@ -1,245 +0,0 @@
|
|||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
import secrets
|
|
||||||
import subprocess
|
|
||||||
from typing import Optional, Type
|
|
||||||
|
|
||||||
import irctest
|
|
||||||
from irctest.basecontrollers import BaseServicesController, DirectoryBasedController
|
|
||||||
import irctest.cases
|
|
||||||
import irctest.runner
|
|
||||||
|
|
||||||
TEMPLATE_DLK_CONFIG = """\
|
|
||||||
info {{
|
|
||||||
SID "00A";
|
|
||||||
network-name "testnetwork";
|
|
||||||
services-name "services.example.org";
|
|
||||||
admin-email "admin@example.org";
|
|
||||||
}}
|
|
||||||
|
|
||||||
link {{
|
|
||||||
hostname "{server_hostname}";
|
|
||||||
port "{server_port}";
|
|
||||||
password "password";
|
|
||||||
}}
|
|
||||||
|
|
||||||
log {{
|
|
||||||
debug "yes";
|
|
||||||
}}
|
|
||||||
|
|
||||||
sql {{
|
|
||||||
port "3306";
|
|
||||||
username "pifpaf";
|
|
||||||
password "pifpaf";
|
|
||||||
database "pifpaf";
|
|
||||||
sockfile "{mysql_socket}";
|
|
||||||
prefix "{dlk_prefix}";
|
|
||||||
}}
|
|
||||||
|
|
||||||
wordpress {{
|
|
||||||
prefix "{wp_prefix}";
|
|
||||||
}}
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
TEMPLATE_DLK_WP_CONFIG = """
|
|
||||||
<?php
|
|
||||||
|
|
||||||
global $wpconfig;
|
|
||||||
$wpconfig = [
|
|
||||||
|
|
||||||
"dbprefix" => "{wp_prefix}",
|
|
||||||
|
|
||||||
|
|
||||||
"default_avatar" => "https://valware.uk/wp-content/plugins/ultimate-member/assets/img/default_avatar.jpg",
|
|
||||||
"forumschan" => "#DLK-Support",
|
|
||||||
|
|
||||||
];
|
|
||||||
"""
|
|
||||||
|
|
||||||
TEMPLATE_WP_CONFIG = """
|
|
||||||
define( 'DB_NAME', 'pifpaf' );
|
|
||||||
define( 'DB_USER', 'pifpaf' );
|
|
||||||
define( 'DB_PASSWORD', 'pifpaf' );
|
|
||||||
define( 'DB_HOST', 'localhost:{mysql_socket}' );
|
|
||||||
define( 'DB_CHARSET', 'utf8' );
|
|
||||||
define( 'DB_COLLATE', '' );
|
|
||||||
|
|
||||||
define( 'AUTH_KEY', 'put your unique phrase here' );
|
|
||||||
define( 'SECURE_AUTH_KEY', 'put your unique phrase here' );
|
|
||||||
define( 'LOGGED_IN_KEY', 'put your unique phrase here' );
|
|
||||||
define( 'NONCE_KEY', 'put your unique phrase here' );
|
|
||||||
define( 'AUTH_SALT', 'put your unique phrase here' );
|
|
||||||
define( 'SECURE_AUTH_SALT', 'put your unique phrase here' );
|
|
||||||
define( 'LOGGED_IN_SALT', 'put your unique phrase here' );
|
|
||||||
define( 'NONCE_SALT', 'put your unique phrase here' );
|
|
||||||
|
|
||||||
$table_prefix = '{wp_prefix}';
|
|
||||||
|
|
||||||
define( 'WP_DEBUG', false );
|
|
||||||
|
|
||||||
if (!defined('ABSPATH')) {{
|
|
||||||
define( 'ABSPATH', '{wp_path}' );
|
|
||||||
}}
|
|
||||||
|
|
||||||
/* That's all, stop editing! Happy publishing. */
|
|
||||||
|
|
||||||
/** Absolute path to the WordPress directory. */
|
|
||||||
|
|
||||||
|
|
||||||
/** Sets up WordPress vars and included files. */
|
|
||||||
require_once ABSPATH . 'wp-settings.php';
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class DlkController(BaseServicesController, DirectoryBasedController):
|
|
||||||
"""Mixin for server controllers that rely on DLK"""
|
|
||||||
|
|
||||||
software_name = "Dlk-Services"
|
|
||||||
|
|
||||||
def run_sql(self, sql: str) -> None:
|
|
||||||
mysql_socket = os.environ["PIFPAF_MYSQL_SOCKET"]
|
|
||||||
subprocess.run(
|
|
||||||
["mysql", "-S", mysql_socket, "pifpaf"],
|
|
||||||
input=sql.encode(),
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
|
|
||||||
self.create_config()
|
|
||||||
|
|
||||||
if protocol == "unreal4":
|
|
||||||
protocol = "unreal5"
|
|
||||||
assert protocol in ("unreal5",), protocol
|
|
||||||
|
|
||||||
mysql_socket = os.environ["PIFPAF_MYSQL_SOCKET"]
|
|
||||||
|
|
||||||
assert self.directory
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.wp_cli_path = Path(os.environ["IRCTEST_WP_CLI_PATH"])
|
|
||||||
if not self.wp_cli_path.is_file():
|
|
||||||
raise KeyError()
|
|
||||||
except KeyError:
|
|
||||||
raise RuntimeError(
|
|
||||||
"$IRCTEST_WP_CLI_PATH must be set to a WP-CLI executable (eg. "
|
|
||||||
"downloaded from <https://raw.githubusercontent.com/wp-cli/builds/"
|
|
||||||
"gh-pages/phar/wp-cli.phar>)"
|
|
||||||
) from None
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.dlk_path = Path(os.environ["IRCTEST_DLK_PATH"])
|
|
||||||
if not self.dlk_path.is_dir():
|
|
||||||
raise KeyError()
|
|
||||||
except KeyError:
|
|
||||||
raise RuntimeError("$IRCTEST_DLK_PATH is not set") from None
|
|
||||||
self.dlk_path = self.dlk_path.resolve()
|
|
||||||
|
|
||||||
# Unpack a fresh Wordpress install in the temporary directory.
|
|
||||||
# In theory we could have a common Wordpress install and only wp-config.php
|
|
||||||
# in the temporary directory; but wp-cli assumes wp-config.php must be
|
|
||||||
# in a Wordpress directory, and fails in various places if it isn't.
|
|
||||||
# Rather than symlinking everything to make it work, let's just copy
|
|
||||||
# the whole code, it's not that big.
|
|
||||||
try:
|
|
||||||
wp_zip_path = Path(os.environ["IRCTEST_WP_ZIP_PATH"])
|
|
||||||
if not wp_zip_path.is_file():
|
|
||||||
raise KeyError()
|
|
||||||
except KeyError:
|
|
||||||
raise RuntimeError(
|
|
||||||
"$IRCTEST_WP_ZIP_PATH must be set to a Wordpress source zipball "
|
|
||||||
"(eg. downloaded from <https://wordpress.org/latest.zip>)"
|
|
||||||
) from None
|
|
||||||
subprocess.run(
|
|
||||||
["unzip", wp_zip_path, "-d", self.directory], stdout=subprocess.DEVNULL
|
|
||||||
)
|
|
||||||
self.wp_path = self.directory / "wordpress"
|
|
||||||
|
|
||||||
rand_hex = secrets.token_hex(6)
|
|
||||||
self.wp_prefix = f"wp{rand_hex}_"
|
|
||||||
self.dlk_prefix = f"dlk{rand_hex}_"
|
|
||||||
template_vars = dict(
|
|
||||||
protocol=protocol,
|
|
||||||
server_hostname=server_hostname,
|
|
||||||
server_port=server_port,
|
|
||||||
mysql_socket=mysql_socket,
|
|
||||||
wp_path=self.wp_path,
|
|
||||||
wp_prefix=self.wp_prefix,
|
|
||||||
dlk_prefix=self.dlk_prefix,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Configure Wordpress
|
|
||||||
wp_config_path = self.directory / "wp-config.php"
|
|
||||||
with open(wp_config_path, "w") as fd:
|
|
||||||
fd.write(TEMPLATE_WP_CONFIG.format(**template_vars))
|
|
||||||
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
"php",
|
|
||||||
self.wp_cli_path,
|
|
||||||
"core",
|
|
||||||
"install",
|
|
||||||
"--url=http://localhost/",
|
|
||||||
"--title=irctest site",
|
|
||||||
"--admin_user=adminuser",
|
|
||||||
"--admin_email=adminuser@example.org",
|
|
||||||
f"--path={self.wp_path}",
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Configure Dlk
|
|
||||||
dlk_log_dir = self.directory / "logs"
|
|
||||||
dlk_conf_dir = self.directory / "conf"
|
|
||||||
dlk_conf_path = dlk_conf_dir / "dalek.conf"
|
|
||||||
os.mkdir(dlk_conf_dir)
|
|
||||||
with open(dlk_conf_path, "w") as fd:
|
|
||||||
fd.write(TEMPLATE_DLK_CONFIG.format(**template_vars))
|
|
||||||
dlk_wp_config_path = dlk_conf_dir / "wordpress.conf"
|
|
||||||
with open(dlk_wp_config_path, "w") as fd:
|
|
||||||
fd.write(TEMPLATE_DLK_WP_CONFIG.format(**template_vars))
|
|
||||||
(dlk_conf_dir / "modules.conf").symlink_to(self.dlk_path / "conf/modules.conf")
|
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
|
||||||
[
|
|
||||||
"php",
|
|
||||||
"src/dalek",
|
|
||||||
],
|
|
||||||
cwd=self.dlk_path,
|
|
||||||
env={
|
|
||||||
**os.environ,
|
|
||||||
"DALEK_CONF_DIR": str(dlk_conf_dir),
|
|
||||||
"DALEK_LOG_DIR": str(dlk_log_dir),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
def terminate(self) -> None:
|
|
||||||
super().terminate()
|
|
||||||
|
|
||||||
def kill(self) -> None:
|
|
||||||
super().kill()
|
|
||||||
|
|
||||||
def registerUser(
|
|
||||||
self,
|
|
||||||
case: irctest.cases.BaseServerTestCase,
|
|
||||||
username: str,
|
|
||||||
password: Optional[str] = None,
|
|
||||||
) -> None:
|
|
||||||
assert password
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
"php",
|
|
||||||
self.wp_cli_path,
|
|
||||||
"user",
|
|
||||||
"create",
|
|
||||||
username,
|
|
||||||
f"{username}@example.org",
|
|
||||||
f"--user_pass={password}",
|
|
||||||
f"--path={self.wp_path}",
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[DlkController]:
|
|
||||||
return DlkController
|
|
@ -1,11 +1,14 @@
|
|||||||
import copy
|
import copy
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
|
||||||
import subprocess
|
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
|
from irctest.cases import BaseServerTestCase
|
||||||
|
|
||||||
BASE_CONFIG = {
|
BASE_CONFIG = {
|
||||||
@ -126,7 +129,7 @@ def hash_password(password: Union[str, bytes]) -> str:
|
|||||||
["ergo", "genpasswd"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
["ergo", "genpasswd"], stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
||||||
)
|
)
|
||||||
out, _ = p.communicate(input_)
|
out, _ = p.communicate(input_)
|
||||||
return out.decode("utf-8").strip()
|
return out.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
class ErgoController(BaseServerController, DirectoryBasedController):
|
class ErgoController(BaseServerController, DirectoryBasedController):
|
||||||
@ -149,9 +152,16 @@ class ErgoController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str],
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
restricted_metadata_keys: Optional[Set[str]] = None,
|
||||||
config: Optional[Any] = None,
|
config: Optional[Any] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if valid_metadata_keys or invalid_metadata_keys:
|
||||||
|
raise NotImplementedByController(
|
||||||
|
"Defining valid and invalid METADATA keys."
|
||||||
|
)
|
||||||
|
|
||||||
self.create_config()
|
self.create_config()
|
||||||
if config is None:
|
if config is None:
|
||||||
config = copy.deepcopy(BASE_CONFIG)
|
config = copy.deepcopy(BASE_CONFIG)
|
||||||
@ -173,32 +183,27 @@ class ErgoController(BaseServerController, DirectoryBasedController):
|
|||||||
bind_address = "127.0.0.1:%s" % (port,)
|
bind_address = "127.0.0.1:%s" % (port,)
|
||||||
listener_conf = None # plaintext
|
listener_conf = None # plaintext
|
||||||
if ssl:
|
if ssl:
|
||||||
self.key_path = self.directory / "ssl.key"
|
self.key_path = os.path.join(self.directory, "ssl.key")
|
||||||
self.pem_path = self.directory / "ssl.pem"
|
self.pem_path = os.path.join(self.directory, "ssl.pem")
|
||||||
listener_conf = {"tls": {"cert": self.pem_path, "key": self.key_path}}
|
listener_conf = {"tls": {"cert": self.pem_path, "key": self.key_path}}
|
||||||
config["server"]["listeners"][bind_address] = listener_conf # type: ignore
|
config["server"]["listeners"][bind_address] = listener_conf # type: ignore
|
||||||
|
|
||||||
config["datastore"]["path"] = str(self.directory / "ircd.db") # type: ignore
|
config["datastore"]["path"] = os.path.join( # type: ignore
|
||||||
|
self.directory, "ircd.db"
|
||||||
|
)
|
||||||
|
|
||||||
if password is not None:
|
if password is not None:
|
||||||
config["server"]["password"] = hash_password(password) # type: ignore
|
config["server"]["password"] = hash_password(password) # type: ignore
|
||||||
|
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
|
|
||||||
self._config_path = self.directory / "server.yml"
|
self._config_path = os.path.join(self.directory, "server.yml")
|
||||||
self._config = config
|
self._config = config
|
||||||
self._write_config()
|
self._write_config()
|
||||||
subprocess.call(["ergo", "initdb", "--conf", self._config_path, "--quiet"])
|
subprocess.call(["ergo", "initdb", "--conf", self._config_path, "--quiet"])
|
||||||
subprocess.call(["ergo", "mkcerts", "--conf", self._config_path, "--quiet"])
|
subprocess.call(["ergo", "mkcerts", "--conf", self._config_path, "--quiet"])
|
||||||
|
|
||||||
if faketime and shutil.which("faketime"):
|
|
||||||
faketime_cmd = ["faketime", "-f", faketime]
|
|
||||||
self.faketime_enabled = True
|
|
||||||
else:
|
|
||||||
faketime_cmd = []
|
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[*faketime_cmd, "ergo", "run", "--conf", self._config_path, "--quiet"]
|
["ergo", "run", "--conf", self._config_path, "--quiet"]
|
||||||
)
|
)
|
||||||
|
|
||||||
def wait_for_services(self) -> None:
|
def wait_for_services(self) -> None:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Optional, Tuple, Type
|
from typing import Optional, Set, Tuple, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import BaseServerController
|
from irctest.basecontrollers import BaseServerController
|
||||||
|
|
||||||
@ -39,7 +39,9 @@ class ExternalServerController(BaseServerController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str],
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
restricted_metadata_keys: Optional[Set[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ class {{
|
|||||||
}};
|
}};
|
||||||
connect {{
|
connect {{
|
||||||
name = "services.example.org";
|
name = "services.example.org";
|
||||||
host = "127.0.0.1"; # Used to validate incoming connection
|
host = "localhost"; # Used to validate incoming connection
|
||||||
port = 0; # We don't need servers to connect to services
|
port = 0; # We don't need servers to connect to services
|
||||||
send_password = "password";
|
send_password = "password";
|
||||||
accept_password = "password";
|
accept_password = "password";
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
import functools
|
import os
|
||||||
import shutil
|
|
||||||
import subprocess
|
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 = """
|
TEMPLATE_CONFIG = """
|
||||||
# Clients:
|
# Clients:
|
||||||
@ -13,14 +17,12 @@ TEMPLATE_CONFIG = """
|
|||||||
resolvehostnames="no" # Faster
|
resolvehostnames="no" # Faster
|
||||||
recvq="40960" # Needs to be larger than a valid message with tags
|
recvq="40960" # Needs to be larger than a valid message with tags
|
||||||
timeout="10" # So tests don't hang too long
|
timeout="10" # So tests don't hang too long
|
||||||
localmax="1000"
|
|
||||||
globalmax="1000"
|
|
||||||
{password_field}>
|
{password_field}>
|
||||||
|
|
||||||
<class
|
<class
|
||||||
name="ServerOperators"
|
name="ServerOperators"
|
||||||
commands="WALLOPS GLOBOPS"
|
commands="WALLOPS GLOBOPS"
|
||||||
privs="channels/auspex users/auspex channels/auspex servers/auspex"
|
privs="channels/auspex users/auspex channels/auspex servers/auspex users/mass-message"
|
||||||
>
|
>
|
||||||
<type
|
<type
|
||||||
name="NetAdmin"
|
name="NetAdmin"
|
||||||
@ -55,10 +57,8 @@ TEMPLATE_CONFIG = """
|
|||||||
target="services.example.org">
|
target="services.example.org">
|
||||||
|
|
||||||
# Protocol:
|
# Protocol:
|
||||||
<module name="banexception">
|
|
||||||
<module name="botmode">
|
<module name="botmode">
|
||||||
<module name="cap">
|
<module name="cap">
|
||||||
<module name="inviteexception">
|
|
||||||
<module name="ircv3">
|
<module name="ircv3">
|
||||||
<module name="ircv3_accounttag">
|
<module name="ircv3_accounttag">
|
||||||
<module name="ircv3_batch">
|
<module name="ircv3_batch">
|
||||||
@ -73,16 +73,10 @@ TEMPLATE_CONFIG = """
|
|||||||
<module name="m_muteban"> # for testing mute extbans
|
<module name="m_muteban"> # for testing mute extbans
|
||||||
<module name="namesx"> # For multi-prefix
|
<module name="namesx"> # For multi-prefix
|
||||||
<module name="sasl">
|
<module name="sasl">
|
||||||
<module name="uhnames"> # For userhost-in-names
|
|
||||||
|
|
||||||
# HELP/HELPOP
|
|
||||||
<module name="alias"> # for the HELP alias
|
|
||||||
<module name="{help_module_name}">
|
|
||||||
<include file="examples/{help_module_name}.conf.example">
|
|
||||||
|
|
||||||
# Misc:
|
# Misc:
|
||||||
<log method="file" type="*" level="debug" target="/tmp/ircd-{port}.log">
|
<log method="file" type="*" level="debug" target="/tmp/ircd-{port}.log">
|
||||||
<server name="My.Little.Server" description="test server" id="000" network="testnet">
|
<server name="My.Little.Server" description="testnet" id="000" network="testnet">
|
||||||
"""
|
"""
|
||||||
|
|
||||||
TEMPLATE_SSL_CONFIG = """
|
TEMPLATE_SSL_CONFIG = """
|
||||||
@ -91,17 +85,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):
|
class InspircdController(BaseServerController, DirectoryBasedController):
|
||||||
software_name = "InspIRCd"
|
software_name = "InspIRCd"
|
||||||
supported_sasl_mechanisms = {"PLAIN"}
|
supported_sasl_mechanisms = {"PLAIN"}
|
||||||
@ -121,13 +104,19 @@ class InspircdController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str] = None,
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
restricted_metadata_keys: Optional[Set[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if valid_metadata_keys or invalid_metadata_keys:
|
||||||
|
raise NotImplementedByController(
|
||||||
|
"Defining valid and invalid METADATA keys."
|
||||||
|
)
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.create_config()
|
self.create_config()
|
||||||
(services_hostname, services_port) = self.get_hostname_and_port()
|
(services_hostname, services_port) = find_hostname_and_port()
|
||||||
|
|
||||||
password_field = 'password="{}"'.format(password) if password else ""
|
password_field = 'password="{}"'.format(password) if password else ""
|
||||||
|
|
||||||
@ -139,13 +128,6 @@ class InspircdController(BaseServerController, DirectoryBasedController):
|
|||||||
else:
|
else:
|
||||||
ssl_config = ""
|
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:
|
with self.open_file("server.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
TEMPLATE_CONFIG.format(
|
TEMPLATE_CONFIG.format(
|
||||||
@ -155,24 +137,15 @@ class InspircdController(BaseServerController, DirectoryBasedController):
|
|||||||
services_port=services_port,
|
services_port=services_port,
|
||||||
password_field=password_field,
|
password_field=password_field,
|
||||||
ssl_config=ssl_config,
|
ssl_config=ssl_config,
|
||||||
help_module_name=help_module_name,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert self.directory
|
assert self.directory
|
||||||
|
|
||||||
if faketime and shutil.which("faketime"):
|
|
||||||
faketime_cmd = ["faketime", "-f", faketime]
|
|
||||||
self.faketime_enabled = True
|
|
||||||
else:
|
|
||||||
faketime_cmd = []
|
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
|
||||||
"inspircd",
|
"inspircd",
|
||||||
"--nofork",
|
"--nofork",
|
||||||
"--config",
|
"--config",
|
||||||
self.directory / "server.conf",
|
os.path.join(self.directory, "server.conf"),
|
||||||
],
|
],
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import shutil
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Set, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import (
|
||||||
BaseServerController,
|
BaseServerController,
|
||||||
@ -10,7 +10,7 @@ from irctest.basecontrollers import (
|
|||||||
|
|
||||||
TEMPLATE_CONFIG = """
|
TEMPLATE_CONFIG = """
|
||||||
# M:<Server NAME>:<YOUR Internet IP#>:<Geographic Location>:<Port>:<SID>:
|
# M:<Server NAME>:<YOUR Internet IP#>:<Geographic Location>:<Port>:<SID>:
|
||||||
M:My.Little.Server:{hostname}:test server:{port}:0042:
|
M:My.Little.Server:{hostname}:Somewhere:{port}:0042:
|
||||||
|
|
||||||
# A:<Your Name/Location>:<Your E-Mail Addr>:<other info>::<network name>:
|
# A:<Your Name/Location>:<Your E-Mail Addr>:<other info>::<network name>:
|
||||||
A:Organization, IRC dept.:Daemon <ircd@example.irc.org>:Client Server::IRCnet:
|
A:Organization, IRC dept.:Daemon <ircd@example.irc.org>:Client Server::IRCnet:
|
||||||
@ -29,8 +29,8 @@ O:*:operpassword:operuser::::
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class Irc2Controller(BaseServerController, DirectoryBasedController):
|
class Ircu2Controller(BaseServerController, DirectoryBasedController):
|
||||||
software_name = "irc2"
|
binary_name: str
|
||||||
services_protocol: str
|
services_protocol: str
|
||||||
|
|
||||||
supports_sts = False
|
supports_sts = False
|
||||||
@ -49,8 +49,13 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str],
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if valid_metadata_keys or invalid_metadata_keys:
|
||||||
|
raise NotImplementedByController(
|
||||||
|
"Defining valid and invalid METADATA keys."
|
||||||
|
)
|
||||||
if ssl:
|
if ssl:
|
||||||
raise NotImplementedByController("TLS")
|
raise NotImplementedByController("TLS")
|
||||||
if run_services:
|
if run_services:
|
||||||
@ -61,7 +66,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
self.create_config()
|
self.create_config()
|
||||||
password_field = password if password else ""
|
password_field = password if password else ""
|
||||||
assert self.directory
|
assert self.directory
|
||||||
pidfile = self.directory / "ircd.pid"
|
pidfile = os.path.join(self.directory, "ircd.pid")
|
||||||
with self.open_file("server.conf") as fd:
|
with self.open_file("server.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
TEMPLATE_CONFIG.format(
|
TEMPLATE_CONFIG.format(
|
||||||
@ -71,26 +76,18 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
pidfile=pidfile,
|
pidfile=pidfile,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if faketime and shutil.which("faketime"):
|
|
||||||
faketime_cmd = ["faketime", "-f", faketime]
|
|
||||||
self.faketime_enabled = True
|
|
||||||
else:
|
|
||||||
faketime_cmd = []
|
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
|
||||||
"ircd",
|
"ircd",
|
||||||
"-s", # no iauth
|
"-s", # no iauth
|
||||||
"-p",
|
"-p",
|
||||||
"on",
|
"on",
|
||||||
"-f",
|
"-f",
|
||||||
self.directory / "server.conf",
|
os.path.join(self.directory, "server.conf"),
|
||||||
],
|
],
|
||||||
# stderr=subprocess.DEVNULL,
|
# stderr=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[Irc2Controller]:
|
def get_irctest_controller_class() -> Type[Ircu2Controller]:
|
||||||
return Irc2Controller
|
return Ircu2Controller
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import shutil
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Set, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import (
|
||||||
BaseServerController,
|
BaseServerController,
|
||||||
@ -51,7 +51,6 @@ features {{
|
|||||||
|
|
||||||
|
|
||||||
class Ircu2Controller(BaseServerController, DirectoryBasedController):
|
class Ircu2Controller(BaseServerController, DirectoryBasedController):
|
||||||
software_name = "ircu2"
|
|
||||||
supports_sts = False
|
supports_sts = False
|
||||||
extban_mute_char = None
|
extban_mute_char = None
|
||||||
|
|
||||||
@ -68,8 +67,13 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str],
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if valid_metadata_keys or invalid_metadata_keys:
|
||||||
|
raise NotImplementedByController(
|
||||||
|
"Defining valid and invalid METADATA keys."
|
||||||
|
)
|
||||||
if ssl:
|
if ssl:
|
||||||
raise NotImplementedByController("TLS")
|
raise NotImplementedByController("TLS")
|
||||||
if run_services:
|
if run_services:
|
||||||
@ -80,7 +84,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
self.create_config()
|
self.create_config()
|
||||||
password_field = 'password = "{}";'.format(password) if password else ""
|
password_field = 'password = "{}";'.format(password) if password else ""
|
||||||
assert self.directory
|
assert self.directory
|
||||||
pidfile = self.directory / "ircd.pid"
|
pidfile = os.path.join(self.directory, "ircd.pid")
|
||||||
with self.open_file("server.conf") as fd:
|
with self.open_file("server.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
TEMPLATE_CONFIG.format(
|
TEMPLATE_CONFIG.format(
|
||||||
@ -90,20 +94,12 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
|
|||||||
pidfile=pidfile,
|
pidfile=pidfile,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if faketime and shutil.which("faketime"):
|
|
||||||
faketime_cmd = ["faketime", "-f", faketime]
|
|
||||||
self.faketime_enabled = True
|
|
||||||
else:
|
|
||||||
faketime_cmd = []
|
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
|
||||||
"ircd",
|
"ircd",
|
||||||
"-n", # don't detach
|
"-n", # don't detach
|
||||||
"-f",
|
"-f",
|
||||||
self.directory / "server.conf",
|
os.path.join(self.directory, "server.conf"),
|
||||||
"-x",
|
"-x",
|
||||||
"DEBUG",
|
"DEBUG",
|
||||||
],
|
],
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Type
|
||||||
|
|
||||||
@ -54,19 +55,13 @@ class LimnoriaController(BaseClientController, DirectoryBasedController):
|
|||||||
# Runs a client with the config given as arguments
|
# Runs a client with the config given as arguments
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.create_config()
|
self.create_config()
|
||||||
|
|
||||||
username = password = ""
|
|
||||||
mechanisms = ""
|
|
||||||
if auth:
|
if auth:
|
||||||
mechanisms = " ".join(mech.to_string() for mech in auth.mechanisms)
|
mechanisms = " ".join(mech.to_string() for mech in auth.mechanisms)
|
||||||
if auth.ecdsa_key:
|
if auth.ecdsa_key:
|
||||||
with self.open_file("ecdsa_key.pem") as fd:
|
with self.open_file("ecdsa_key.pem") as fd:
|
||||||
fd.write(auth.ecdsa_key)
|
fd.write(auth.ecdsa_key)
|
||||||
|
else:
|
||||||
if auth.username:
|
mechanisms = ""
|
||||||
username = auth.username.encode("unicode_escape").decode()
|
|
||||||
if auth.password:
|
|
||||||
password = auth.password.encode("unicode_escape").decode()
|
|
||||||
with self.open_file("bot.conf") as fd:
|
with self.open_file("bot.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
TEMPLATE_CONFIG.format(
|
TEMPLATE_CONFIG.format(
|
||||||
@ -74,8 +69,8 @@ class LimnoriaController(BaseClientController, DirectoryBasedController):
|
|||||||
loglevel="CRITICAL",
|
loglevel="CRITICAL",
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
port=port,
|
port=port,
|
||||||
username=username,
|
username=auth.username if auth else "",
|
||||||
password=password,
|
password=auth.password if auth else "",
|
||||||
mechanisms=mechanisms.lower(),
|
mechanisms=mechanisms.lower(),
|
||||||
enable_tls=tls_config.enable if tls_config else "False",
|
enable_tls=tls_config.enable if tls_config else "False",
|
||||||
trusted_fingerprints=" ".join(tls_config.trusted_fingerprints)
|
trusted_fingerprints=" ".join(tls_config.trusted_fingerprints)
|
||||||
@ -84,7 +79,9 @@ class LimnoriaController(BaseClientController, DirectoryBasedController):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
assert self.directory
|
assert self.directory
|
||||||
self.proc = subprocess.Popen(["supybot", self.directory / "bot.conf"])
|
self.proc = subprocess.Popen(
|
||||||
|
["supybot", os.path.join(self.directory, "bot.conf")]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[LimnoriaController]:
|
def get_irctest_controller_class() -> Type[LimnoriaController]:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import shutil
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Set, Type
|
from typing import Optional, Set, Type
|
||||||
|
|
||||||
@ -33,10 +33,10 @@ extensions:
|
|||||||
- mammon.ext.ircv3.sasl
|
- mammon.ext.ircv3.sasl
|
||||||
- mammon.ext.misc.nopost
|
- mammon.ext.misc.nopost
|
||||||
metadata:
|
metadata:
|
||||||
restricted_keys: []
|
restricted_keys:
|
||||||
|
{restricted_keys}
|
||||||
whitelist:
|
whitelist:
|
||||||
- display-name
|
{authorized_keys}
|
||||||
- avatar
|
|
||||||
monitor:
|
monitor:
|
||||||
limit: 20
|
limit: 20
|
||||||
motd:
|
motd:
|
||||||
@ -89,7 +89,9 @@ class MammonController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str],
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
restricted_metadata_keys: Optional[Set[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
if password is not None:
|
if password is not None:
|
||||||
raise NotImplementedByController("PASS command")
|
raise NotImplementedByController("PASS command")
|
||||||
@ -104,25 +106,19 @@ class MammonController(BaseServerController, DirectoryBasedController):
|
|||||||
directory=self.directory,
|
directory=self.directory,
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
port=port,
|
port=port,
|
||||||
|
authorized_keys=make_list(valid_metadata_keys or set()),
|
||||||
|
restricted_keys=make_list(restricted_metadata_keys or set()),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# with self.open_file('server.yml', 'r') as fd:
|
# with self.open_file('server.yml', 'r') as fd:
|
||||||
# print(fd.read())
|
# print(fd.read())
|
||||||
assert self.directory
|
assert self.directory
|
||||||
|
|
||||||
if faketime and shutil.which("faketime"):
|
|
||||||
faketime_cmd = ["faketime", "-f", faketime]
|
|
||||||
self.faketime_enabled = True
|
|
||||||
else:
|
|
||||||
faketime_cmd = []
|
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
|
||||||
"mammond",
|
"mammond",
|
||||||
"--nofork", # '--debug',
|
"--nofork", # '--debug',
|
||||||
"--config",
|
"--config",
|
||||||
self.directory / "server.yml",
|
os.path.join(self.directory, "server.yml"),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
from typing import Type
|
|
||||||
|
|
||||||
from .ircu2 import Ircu2Controller
|
|
||||||
|
|
||||||
|
|
||||||
class NefariousController(Ircu2Controller):
|
|
||||||
software_name = "Nefarious"
|
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[NefariousController]:
|
|
||||||
return NefariousController
|
|
@ -1,13 +1,18 @@
|
|||||||
import shutil
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Set, Type
|
from typing import Optional, Set, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import BaseServerController, DirectoryBasedController
|
from irctest.basecontrollers import (
|
||||||
|
BaseServerController,
|
||||||
|
DirectoryBasedController,
|
||||||
|
NotImplementedByController,
|
||||||
|
)
|
||||||
|
from irctest.irc_utils.junkdrawer import find_hostname_and_port
|
||||||
|
|
||||||
TEMPLATE_CONFIG = """
|
TEMPLATE_CONFIG = """
|
||||||
[Global]
|
[Global]
|
||||||
Name = My.Little.Server
|
Name = My.Little.Server
|
||||||
Info = test server
|
Info = ExampleNET Server
|
||||||
Bind = {hostname}
|
Bind = {hostname}
|
||||||
Ports = {port}
|
Ports = {port}
|
||||||
AdminInfo1 = Bob Smith
|
AdminInfo1 = Bob Smith
|
||||||
@ -21,9 +26,6 @@ TEMPLATE_CONFIG = """
|
|||||||
Passive = yes # don't connect to it
|
Passive = yes # don't connect to it
|
||||||
ServiceMask = *Serv
|
ServiceMask = *Serv
|
||||||
|
|
||||||
[Options]
|
|
||||||
MorePrivacy = no # by default, always replies to WHOWAS with ERR_WASNOSUCHNICK
|
|
||||||
|
|
||||||
[Operator]
|
[Operator]
|
||||||
Name = operuser
|
Name = operuser
|
||||||
Password = operpassword
|
Password = operpassword
|
||||||
@ -48,13 +50,19 @@ class NgircdController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str],
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
restricted_metadata_keys: Optional[Set[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if valid_metadata_keys or invalid_metadata_keys:
|
||||||
|
raise NotImplementedByController(
|
||||||
|
"Defining valid and invalid METADATA keys."
|
||||||
|
)
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.create_config()
|
self.create_config()
|
||||||
(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 ""
|
password_field = "Password = {}".format(password) if password else ""
|
||||||
|
|
||||||
@ -70,7 +78,6 @@ class NgircdController(BaseServerController, DirectoryBasedController):
|
|||||||
fd.write("\n")
|
fd.write("\n")
|
||||||
|
|
||||||
assert self.directory
|
assert self.directory
|
||||||
|
|
||||||
with self.open_file("server.conf") as fd:
|
with self.open_file("server.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
TEMPLATE_CONFIG.format(
|
TEMPLATE_CONFIG.format(
|
||||||
@ -81,23 +88,15 @@ class NgircdController(BaseServerController, DirectoryBasedController):
|
|||||||
password_field=password_field,
|
password_field=password_field,
|
||||||
key_path=self.key_path,
|
key_path=self.key_path,
|
||||||
pem_path=self.pem_path,
|
pem_path=self.pem_path,
|
||||||
empty_file=self.directory / "empty.txt",
|
empty_file=os.path.join(self.directory, "empty.txt"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if faketime and shutil.which("faketime"):
|
|
||||||
faketime_cmd = ["faketime", "-f", faketime]
|
|
||||||
self.faketime_enabled = True
|
|
||||||
else:
|
|
||||||
faketime_cmd = []
|
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
|
||||||
"ngircd",
|
"ngircd",
|
||||||
"--nodaemon",
|
"--nodaemon",
|
||||||
"--config",
|
"--config",
|
||||||
self.directory / "server.conf",
|
os.path.join(self.directory, "server.conf"),
|
||||||
],
|
],
|
||||||
# stdout=subprocess.DEVNULL,
|
# stdout=subprocess.DEVNULL,
|
||||||
)
|
)
|
||||||
|
@ -45,7 +45,7 @@ class {{
|
|||||||
}};
|
}};
|
||||||
connect {{
|
connect {{
|
||||||
name = "services.example.org";
|
name = "services.example.org";
|
||||||
host = "127.0.0.1"; # Used to validate incoming connection
|
host = "localhost"; # Used to validate incoming connection
|
||||||
port = 0; # We don't need servers to connect to services
|
port = 0; # We don't need servers to connect to services
|
||||||
send_password = "password";
|
send_password = "password";
|
||||||
accept_password = "password";
|
accept_password = "password";
|
||||||
@ -74,7 +74,7 @@ operator {{
|
|||||||
|
|
||||||
|
|
||||||
class Plexus4Controller(BaseHybridController):
|
class Plexus4Controller(BaseHybridController):
|
||||||
software_name = "Plexus4"
|
software_name = "Hybrid"
|
||||||
binary_name = "ircd"
|
binary_name = "ircd"
|
||||||
services_protocol = "plexus"
|
services_protocol = "plexus"
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import shutil
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Type
|
from typing import Optional, Set, Type
|
||||||
|
|
||||||
from irctest.basecontrollers import (
|
from irctest.basecontrollers import (
|
||||||
BaseServerController,
|
BaseServerController,
|
||||||
@ -67,8 +67,13 @@ class SnircdController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str],
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if valid_metadata_keys or invalid_metadata_keys:
|
||||||
|
raise NotImplementedByController(
|
||||||
|
"Defining valid and invalid METADATA keys."
|
||||||
|
)
|
||||||
if ssl:
|
if ssl:
|
||||||
raise NotImplementedByController("TLS")
|
raise NotImplementedByController("TLS")
|
||||||
if run_services:
|
if run_services:
|
||||||
@ -79,7 +84,7 @@ class SnircdController(BaseServerController, DirectoryBasedController):
|
|||||||
self.create_config()
|
self.create_config()
|
||||||
password_field = 'password = "{}";'.format(password) if password else ""
|
password_field = 'password = "{}";'.format(password) if password else ""
|
||||||
assert self.directory
|
assert self.directory
|
||||||
pidfile = self.directory / "ircd.pid"
|
pidfile = os.path.join(self.directory, "ircd.pid")
|
||||||
with self.open_file("server.conf") as fd:
|
with self.open_file("server.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
TEMPLATE_CONFIG.format(
|
TEMPLATE_CONFIG.format(
|
||||||
@ -89,20 +94,12 @@ class SnircdController(BaseServerController, DirectoryBasedController):
|
|||||||
pidfile=pidfile,
|
pidfile=pidfile,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if faketime and shutil.which("faketime"):
|
|
||||||
faketime_cmd = ["faketime", "-f", faketime]
|
|
||||||
self.faketime_enabled = True
|
|
||||||
else:
|
|
||||||
faketime_cmd = []
|
|
||||||
|
|
||||||
self.proc = subprocess.Popen(
|
self.proc = subprocess.Popen(
|
||||||
[
|
[
|
||||||
*faketime_cmd,
|
|
||||||
"ircd",
|
"ircd",
|
||||||
"-n", # don't detach
|
"-n", # don't detach
|
||||||
"-f",
|
"-f",
|
||||||
self.directory / "server.conf",
|
os.path.join(self.directory, "server.conf"),
|
||||||
"-x",
|
"-x",
|
||||||
"DEBUG",
|
"DEBUG",
|
||||||
],
|
],
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from pathlib import Path
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
from typing import Optional, TextIO, Type, cast
|
from typing import Optional, TextIO, Type, cast
|
||||||
@ -38,14 +38,14 @@ class SopelController(BaseClientController):
|
|||||||
super().kill()
|
super().kill()
|
||||||
if self.filename:
|
if self.filename:
|
||||||
try:
|
try:
|
||||||
(Path("~/.sopel/").expanduser() / self.filename).unlink()
|
os.unlink(os.path.join(os.path.expanduser("~/.sopel/"), self.filename))
|
||||||
except OSError: # File does not exist
|
except OSError: # File does not exist
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def open_file(self, filename: str, mode: str = "a") -> TextIO:
|
def open_file(self, filename: str, mode: str = "a") -> TextIO:
|
||||||
dir_path = Path("~/.sopel/").expanduser()
|
dir_path = os.path.expanduser("~/.sopel/")
|
||||||
dir_path.mkdir(parents=True, exist_ok=True)
|
os.makedirs(dir_path, exist_ok=True)
|
||||||
return cast(TextIO, (dir_path / filename).open(mode))
|
return cast(TextIO, open(os.path.join(dir_path, filename), mode))
|
||||||
|
|
||||||
def create_config(self) -> None:
|
def create_config(self) -> None:
|
||||||
with self.open_file(self.filename):
|
with self.open_file(self.filename):
|
||||||
@ -73,7 +73,7 @@ class SopelController(BaseClientController):
|
|||||||
auth_method="auth_method = sasl" if auth else "",
|
auth_method="auth_method = sasl" if auth else "",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.proc = subprocess.Popen(["sopel", "-c", self.filename])
|
self.proc = subprocess.Popen(["sopel", "--quiet", "-c", self.filename])
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[SopelController]:
|
def get_irctest_controller_class() -> Type[SopelController]:
|
||||||
|
@ -1,106 +0,0 @@
|
|||||||
import json
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
from typing import Optional, Type
|
|
||||||
|
|
||||||
from irctest import authentication, tls
|
|
||||||
from irctest.basecontrollers import (
|
|
||||||
BaseClientController,
|
|
||||||
DirectoryBasedController,
|
|
||||||
NotImplementedByController,
|
|
||||||
)
|
|
||||||
|
|
||||||
TEMPLATE_CONFIG = """
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
module.exports = {config};
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TheLoungeController(BaseClientController, DirectoryBasedController):
|
|
||||||
software_name = "TheLounge"
|
|
||||||
supported_sasl_mechanisms = {
|
|
||||||
"PLAIN",
|
|
||||||
"ECDSA-NIST256P-CHALLENGE",
|
|
||||||
"SCRAM-SHA-256",
|
|
||||||
"EXTERNAL",
|
|
||||||
}
|
|
||||||
supports_sts = True
|
|
||||||
|
|
||||||
def create_config(self) -> None:
|
|
||||||
super().create_config()
|
|
||||||
with self.open_file("bot.conf"):
|
|
||||||
pass
|
|
||||||
with self.open_file("conf/users.conf"):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def run(
|
|
||||||
self,
|
|
||||||
hostname: str,
|
|
||||||
port: int,
|
|
||||||
auth: Optional[authentication.Authentication],
|
|
||||||
tls_config: Optional[tls.TlsConfig] = None,
|
|
||||||
) -> None:
|
|
||||||
if tls_config is None:
|
|
||||||
tls_config = tls.TlsConfig(enable=False, trusted_fingerprints=[])
|
|
||||||
if tls_config and tls_config.trusted_fingerprints:
|
|
||||||
raise NotImplementedByController("Trusted fingerprints.")
|
|
||||||
if auth and any(
|
|
||||||
mech.to_string().startswith(("SCRAM-", "ECDSA-"))
|
|
||||||
for mech in auth.mechanisms
|
|
||||||
):
|
|
||||||
raise NotImplementedByController("ecdsa")
|
|
||||||
if auth and auth.password and len(auth.password) > 300:
|
|
||||||
# https://github.com/thelounge/thelounge/pull/4480
|
|
||||||
# Note that The Lounge truncates on 300 characters, not bytes.
|
|
||||||
raise NotImplementedByController("Passwords longer than 300 chars")
|
|
||||||
# Runs a client with the config given as arguments
|
|
||||||
assert self.proc is None
|
|
||||||
self.create_config()
|
|
||||||
if auth:
|
|
||||||
mechanisms = " ".join(mech.to_string() for mech in auth.mechanisms)
|
|
||||||
if auth.ecdsa_key:
|
|
||||||
with self.open_file("ecdsa_key.pem") as fd:
|
|
||||||
fd.write(auth.ecdsa_key)
|
|
||||||
else:
|
|
||||||
mechanisms = ""
|
|
||||||
|
|
||||||
assert self.directory
|
|
||||||
with self.open_file("config.js") as fd:
|
|
||||||
fd.write(
|
|
||||||
TEMPLATE_CONFIG.format(
|
|
||||||
config=json.dumps(
|
|
||||||
dict(
|
|
||||||
public=False,
|
|
||||||
host=f"unix:{self.directory}/sock", # prevents binding
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
with self.open_file("users/testuser.json") as fd:
|
|
||||||
json.dump(
|
|
||||||
dict(
|
|
||||||
networks=[
|
|
||||||
dict(
|
|
||||||
name="testnet",
|
|
||||||
host=hostname,
|
|
||||||
port=port,
|
|
||||||
tls=tls_config.enable if tls_config else "False",
|
|
||||||
sasl=mechanisms.lower(),
|
|
||||||
saslAccount=auth.username if auth else "",
|
|
||||||
saslPassword=auth.password if auth else "",
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
fd,
|
|
||||||
)
|
|
||||||
with self.open_file("users/testuser.json", "r") as fd:
|
|
||||||
print("config", json.load(fd)["networks"][0]["saslPassword"])
|
|
||||||
self.proc = subprocess.Popen(
|
|
||||||
[os.environ.get("THELOUNGE_BIN", "thelounge"), "start"],
|
|
||||||
env={**os.environ, "THELOUNGE_HOME": str(self.directory)},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[TheLoungeController]:
|
|
||||||
return TheLoungeController
|
|
@ -1,23 +1,21 @@
|
|||||||
import contextlib
|
import os
|
||||||
import fcntl
|
|
||||||
import functools
|
|
||||||
from pathlib import Path
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import textwrap
|
from typing import Optional, Set, Type
|
||||||
from typing import Callable, ContextManager, Iterator, Optional, 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 = """
|
TEMPLATE_CONFIG = """
|
||||||
include "modules.default.conf";
|
include "modules.default.conf";
|
||||||
include "operclass.default.conf";
|
include "operclass.default.conf";
|
||||||
{extras}
|
|
||||||
include "help/help.conf";
|
|
||||||
|
|
||||||
me {{
|
me {{
|
||||||
name "My.Little.Server";
|
name "My.Little.Server";
|
||||||
info "test server";
|
info "ExampleNET Server";
|
||||||
sid "001";
|
sid "001";
|
||||||
}}
|
}}
|
||||||
admin {{
|
admin {{
|
||||||
@ -89,15 +87,11 @@ set {{
|
|||||||
// Prevent throttling, especially test_buffering.py which
|
// Prevent throttling, especially test_buffering.py which
|
||||||
// triggers anti-flood with its very long lines
|
// triggers anti-flood with its very long lines
|
||||||
unknown-users {{
|
unknown-users {{
|
||||||
nick-flood 255:10;
|
|
||||||
lag-penalty 1;
|
lag-penalty 1;
|
||||||
lag-penalty-bytes 10000;
|
lag-penalty-bytes 10000;
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
modes-on-join "+H 100:1d"; // Enables CHATHISTORY
|
modes-on-join "+H 100:1d"; // Enables CHATHISTORY
|
||||||
|
|
||||||
{set_v6only}
|
|
||||||
|
|
||||||
}}
|
}}
|
||||||
|
|
||||||
tld {{
|
tld {{
|
||||||
@ -107,84 +101,21 @@ tld {{
|
|||||||
rules "{empty_file}";
|
rules "{empty_file}";
|
||||||
}}
|
}}
|
||||||
|
|
||||||
files {{
|
|
||||||
tunefile "{empty_file}";
|
|
||||||
}}
|
|
||||||
|
|
||||||
oper "operuser" {{
|
oper "operuser" {{
|
||||||
password "operpassword";
|
password = "operpassword";
|
||||||
mask *;
|
mask *;
|
||||||
class clients;
|
class clients;
|
||||||
operclass netadmin;
|
operclass netadmin;
|
||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SET_V6ONLY = """
|
|
||||||
// Remove RPL_WHOISSPECIAL used to advertise security groups
|
|
||||||
whois-details {
|
|
||||||
security-groups { everyone none; self none; oper none; }
|
|
||||||
}
|
|
||||||
|
|
||||||
plaintext-policy {
|
|
||||||
server warn; // https://www.unrealircd.org/docs/FAQ#server-requires-tls
|
|
||||||
oper warn; // https://www.unrealircd.org/docs/FAQ#oper-requires-tls
|
|
||||||
}
|
|
||||||
|
|
||||||
anti-flood {
|
|
||||||
everyone {
|
|
||||||
connect-flood 255:10;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
def _filelock(path: Path) -> Callable[[], ContextManager]:
|
|
||||||
"""Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist"""
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def f() -> Iterator[None]:
|
|
||||||
with open(path, "a") as fd:
|
|
||||||
fcntl.flock(fd, fcntl.LOCK_EX)
|
|
||||||
yield
|
|
||||||
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
_UNREALIRCD_BIN = shutil.which("unrealircd")
|
|
||||||
if _UNREALIRCD_BIN:
|
|
||||||
_UNREALIRCD_PREFIX = Path(_UNREALIRCD_BIN).parent.parent
|
|
||||||
|
|
||||||
# Try to keep that lock file specific to this Unrealircd instance
|
|
||||||
_LOCK_PATH = _UNREALIRCD_PREFIX / "irctest-unrealircd-startstop.lock"
|
|
||||||
else:
|
|
||||||
# unrealircd not found; we are probably going to crash later anyway...
|
|
||||||
_LOCK_PATH = Path("/tmp/irctest-unrealircd-startstop.lock")
|
|
||||||
|
|
||||||
_STARTSTOP_LOCK = _filelock(_LOCK_PATH)
|
|
||||||
"""
|
|
||||||
Unreal cleans its tmp/ directory after each run, which prevents
|
|
||||||
multiple processes from starting/stopping at the same time.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
@functools.lru_cache()
|
|
||||||
def installed_version() -> int:
|
|
||||||
output = subprocess.check_output(["unrealircd", "-v"], universal_newlines=True)
|
|
||||||
if output.startswith("UnrealIRCd-5."):
|
|
||||||
return 5
|
|
||||||
elif output.startswith("UnrealIRCd-6."):
|
|
||||||
return 6
|
|
||||||
else:
|
|
||||||
assert False, f"unexpected version: {output}"
|
|
||||||
|
|
||||||
|
|
||||||
class UnrealircdController(BaseServerController, DirectoryBasedController):
|
class UnrealircdController(BaseServerController, DirectoryBasedController):
|
||||||
software_name = "UnrealIRCd"
|
software_name = "UnrealIRCd"
|
||||||
supported_sasl_mechanisms = {"PLAIN"}
|
supported_sasl_mechanisms = {"PLAIN"}
|
||||||
supports_sts = False
|
supports_sts = False
|
||||||
|
|
||||||
extban_mute_char = "quiet" if installed_version() >= 6 else "q"
|
extban_mute_char = "q"
|
||||||
software_version = installed_version()
|
|
||||||
|
|
||||||
def create_config(self) -> None:
|
def create_config(self) -> None:
|
||||||
super().create_config()
|
super().create_config()
|
||||||
@ -199,33 +130,23 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
|
|||||||
password: Optional[str],
|
password: Optional[str],
|
||||||
ssl: bool,
|
ssl: bool,
|
||||||
run_services: bool,
|
run_services: bool,
|
||||||
faketime: Optional[str],
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
restricted_metadata_keys: Optional[Set[str]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if valid_metadata_keys or invalid_metadata_keys:
|
||||||
|
raise NotImplementedByController(
|
||||||
|
"Defining valid and invalid METADATA keys."
|
||||||
|
)
|
||||||
assert self.proc is None
|
assert self.proc is None
|
||||||
self.port = port
|
self.port = port
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.create_config()
|
self.create_config()
|
||||||
|
(unused_hostname, unused_port) = find_hostname_and_port()
|
||||||
if installed_version() >= 6:
|
(services_hostname, services_port) = find_hostname_and_port()
|
||||||
extras = textwrap.dedent(
|
|
||||||
"""
|
|
||||||
include "snomasks.default.conf";
|
|
||||||
loadmodule "cloak_md5";
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
set_v6only = SET_V6ONLY
|
|
||||||
else:
|
|
||||||
extras = ""
|
|
||||||
set_v6only = ""
|
|
||||||
|
|
||||||
with self.open_file("empty.txt") as fd:
|
|
||||||
fd.write("\n")
|
|
||||||
|
|
||||||
password_field = 'password "{}";'.format(password) if password else ""
|
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()
|
self.gen_ssl()
|
||||||
if ssl:
|
if ssl:
|
||||||
(tls_hostname, tls_port) = (hostname, port)
|
(tls_hostname, tls_port) = (hostname, port)
|
||||||
@ -234,8 +155,10 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
|
|||||||
# Unreal refuses to start without TLS enabled
|
# Unreal refuses to start without TLS enabled
|
||||||
(tls_hostname, tls_port) = (unused_hostname, unused_port)
|
(tls_hostname, tls_port) = (unused_hostname, unused_port)
|
||||||
|
|
||||||
assert self.directory
|
with self.open_file("empty.txt") as fd:
|
||||||
|
fd.write("\n")
|
||||||
|
|
||||||
|
assert self.directory
|
||||||
with self.open_file("unrealircd.conf") as fd:
|
with self.open_file("unrealircd.conf") as fd:
|
||||||
fd.write(
|
fd.write(
|
||||||
TEMPLATE_CONFIG.format(
|
TEMPLATE_CONFIG.format(
|
||||||
@ -248,33 +171,22 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
|
|||||||
password_field=password_field,
|
password_field=password_field,
|
||||||
key_path=self.key_path,
|
key_path=self.key_path,
|
||||||
pem_path=self.pem_path,
|
pem_path=self.pem_path,
|
||||||
empty_file=self.directory / "empty.txt",
|
empty_file=os.path.join(self.directory, "empty.txt"),
|
||||||
set_v6only=set_v6only,
|
|
||||||
extras=extras,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
self.proc = subprocess.Popen(
|
||||||
if faketime and shutil.which("faketime"):
|
[
|
||||||
faketime_cmd = ["faketime", "-f", faketime]
|
"unrealircd",
|
||||||
self.faketime_enabled = True
|
"-t",
|
||||||
else:
|
"-F", # BOOT_NOFORK
|
||||||
faketime_cmd = []
|
"-f",
|
||||||
|
os.path.join(self.directory, "unrealircd.conf"),
|
||||||
with _STARTSTOP_LOCK():
|
],
|
||||||
self.proc = subprocess.Popen(
|
# stdout=subprocess.DEVNULL,
|
||||||
[
|
)
|
||||||
*faketime_cmd,
|
|
||||||
"unrealircd",
|
|
||||||
"-t",
|
|
||||||
"-F", # BOOT_NOFORK
|
|
||||||
"-f",
|
|
||||||
self.directory / "unrealircd.conf",
|
|
||||||
],
|
|
||||||
# stdout=subprocess.DEVNULL,
|
|
||||||
)
|
|
||||||
self.wait_for_port()
|
|
||||||
|
|
||||||
if run_services:
|
if run_services:
|
||||||
|
self.wait_for_port()
|
||||||
self.services_controller = self.services_controller_class(
|
self.services_controller = self.services_controller_class(
|
||||||
self.test_config, self
|
self.test_config, self
|
||||||
)
|
)
|
||||||
@ -284,14 +196,6 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
|
|||||||
server_port=services_port,
|
server_port=services_port,
|
||||||
)
|
)
|
||||||
|
|
||||||
def kill_proc(self) -> None:
|
|
||||||
assert self.proc
|
|
||||||
|
|
||||||
with _STARTSTOP_LOCK():
|
|
||||||
self.proc.kill()
|
|
||||||
self.proc.wait(5) # wait for it to actually die
|
|
||||||
self.proc = None
|
|
||||||
|
|
||||||
|
|
||||||
def get_irctest_controller_class() -> Type[UnrealircdController]:
|
def get_irctest_controller_class() -> Type[UnrealircdController]:
|
||||||
return UnrealircdController
|
return UnrealircdController
|
||||||
|
@ -1,465 +0,0 @@
|
|||||||
import base64
|
|
||||||
import dataclasses
|
|
||||||
import gzip
|
|
||||||
import hashlib
|
|
||||||
import importlib
|
|
||||||
from pathlib import Path
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
from typing import (
|
|
||||||
IO,
|
|
||||||
Callable,
|
|
||||||
Dict,
|
|
||||||
Iterable,
|
|
||||||
Iterator,
|
|
||||||
List,
|
|
||||||
Optional,
|
|
||||||
Tuple,
|
|
||||||
TypeVar,
|
|
||||||
Union,
|
|
||||||
)
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
from defusedxml.ElementTree import parse as parse_xml
|
|
||||||
import docutils.core
|
|
||||||
|
|
||||||
from .shortxml import Namespace
|
|
||||||
|
|
||||||
NETLIFY_CHAR_BLACKLIST = frozenset('":<>|*?\r\n#')
|
|
||||||
"""Characters not allowed in output filenames"""
|
|
||||||
|
|
||||||
|
|
||||||
HTML = Namespace("http://www.w3.org/1999/xhtml")
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class CaseResult:
|
|
||||||
module_name: str
|
|
||||||
class_name: str
|
|
||||||
test_name: str
|
|
||||||
job: str
|
|
||||||
success: bool
|
|
||||||
skipped: bool
|
|
||||||
system_out: Optional[str]
|
|
||||||
details: Optional[str] = None
|
|
||||||
type: Optional[str] = None
|
|
||||||
message: Optional[str] = None
|
|
||||||
|
|
||||||
def output_filename(self) -> str:
|
|
||||||
test_name = self.test_name
|
|
||||||
if len(test_name) > 50 or set(test_name) & NETLIFY_CHAR_BLACKLIST:
|
|
||||||
# File name too long or otherwise invalid. This should be good enough:
|
|
||||||
m = re.match(r"(?P<function_name>\w+?)\[(?P<params>.+)\]", test_name)
|
|
||||||
assert m, "File name is too long but has no parameter."
|
|
||||||
test_name = f'{m.group("function_name")}[{md5sum(m.group("params"))}]'
|
|
||||||
return f"{self.job}_{self.module_name}.{self.class_name}.{test_name}.txt"
|
|
||||||
|
|
||||||
|
|
||||||
TK = TypeVar("TK")
|
|
||||||
TV = TypeVar("TV")
|
|
||||||
|
|
||||||
|
|
||||||
def md5sum(text: str) -> str:
|
|
||||||
return base64.urlsafe_b64encode(hashlib.md5(text.encode()).digest()).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def group_by(list_: Iterable[TV], key: Callable[[TV], TK]) -> Dict[TK, List[TV]]:
|
|
||||||
groups: Dict[TK, List[TV]] = {}
|
|
||||||
for value in list_:
|
|
||||||
groups.setdefault(key(value), []).append(value)
|
|
||||||
|
|
||||||
return groups
|
|
||||||
|
|
||||||
|
|
||||||
def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseResult]:
|
|
||||||
(suite,) = job.getroot()
|
|
||||||
for case in suite:
|
|
||||||
if "name" not in case.attrib:
|
|
||||||
continue
|
|
||||||
|
|
||||||
success = True
|
|
||||||
skipped = False
|
|
||||||
details = None
|
|
||||||
system_out = None
|
|
||||||
extra: Dict[str, str] = {}
|
|
||||||
for child in case:
|
|
||||||
if child.tag == "skipped":
|
|
||||||
success = True
|
|
||||||
skipped = True
|
|
||||||
details = None
|
|
||||||
extra = child.attrib
|
|
||||||
elif child.tag in ("failure", "error"):
|
|
||||||
success = False
|
|
||||||
skipped = False
|
|
||||||
details = child.text
|
|
||||||
extra = child.attrib
|
|
||||||
elif child.tag == "system-out":
|
|
||||||
assert (
|
|
||||||
system_out is None
|
|
||||||
# for some reason, skipped tests have two system-out;
|
|
||||||
# and the second one contains test teardown
|
|
||||||
or child.text.startswith(system_out.rstrip())
|
|
||||||
), ("Duplicate system-out tag", repr(system_out), repr(child.text))
|
|
||||||
system_out = child.text
|
|
||||||
else:
|
|
||||||
assert False, child
|
|
||||||
|
|
||||||
(module_name, class_name) = case.attrib["classname"].rsplit(".", 1)
|
|
||||||
m = re.match(
|
|
||||||
r"(.*/)?pytest[ -]results[ _](?P<name>.*)"
|
|
||||||
r"[ _][(]?(stable|release|devel|devel_release)[)]?/pytest.xml(.gz)?",
|
|
||||||
str(job_file_name),
|
|
||||||
)
|
|
||||||
assert m, job_file_name
|
|
||||||
yield CaseResult(
|
|
||||||
module_name=module_name,
|
|
||||||
class_name=class_name,
|
|
||||||
test_name=case.attrib["name"],
|
|
||||||
job=m.group("name"),
|
|
||||||
success=success,
|
|
||||||
skipped=skipped,
|
|
||||||
details=details,
|
|
||||||
system_out=system_out,
|
|
||||||
**extra,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def rst_to_element(s: str) -> ET.Element:
|
|
||||||
html = docutils.core.publish_parts(s, writer_name="xhtml")["html_body"]
|
|
||||||
|
|
||||||
# Force the HTML namespace on all elements produced by docutils, which are
|
|
||||||
# unqualified
|
|
||||||
tree_builder = ET.TreeBuilder(
|
|
||||||
element_factory=lambda tag, attrib: ET.Element(
|
|
||||||
"{%s}%s" % (HTML.uri, tag),
|
|
||||||
{"{%s}%s" % (HTML.uri, k): v for (k, v) in attrib.items()},
|
|
||||||
)
|
|
||||||
)
|
|
||||||
parser = ET.XMLParser(target=tree_builder)
|
|
||||||
|
|
||||||
htmltree = ET.fromstring(html, parser=parser)
|
|
||||||
return htmltree
|
|
||||||
|
|
||||||
|
|
||||||
def docstring(obj: object) -> Optional[ET.Element]:
|
|
||||||
if obj.__doc__ is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return rst_to_element(obj.__doc__)
|
|
||||||
|
|
||||||
|
|
||||||
def build_job_html(job: str, results: List[CaseResult]) -> ET.Element:
|
|
||||||
jobs = sorted({result.job for result in results})
|
|
||||||
|
|
||||||
table = build_test_table(jobs, results, "job-results test-matrix")
|
|
||||||
|
|
||||||
return HTML.html(
|
|
||||||
HTML.head(
|
|
||||||
HTML.title(job),
|
|
||||||
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
|
|
||||||
),
|
|
||||||
HTML.body(
|
|
||||||
HTML.h1(job),
|
|
||||||
table,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_module_html(
|
|
||||||
jobs: List[str], results: List[CaseResult], module_name: str
|
|
||||||
) -> ET.Element:
|
|
||||||
module = importlib.import_module(module_name)
|
|
||||||
|
|
||||||
table = build_test_table(jobs, results, "module-results test-matrix")
|
|
||||||
|
|
||||||
return HTML.html(
|
|
||||||
HTML.head(
|
|
||||||
HTML.title(module_name),
|
|
||||||
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
|
|
||||||
),
|
|
||||||
HTML.body(
|
|
||||||
HTML.h1(module_name),
|
|
||||||
docstring(module),
|
|
||||||
table,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def build_test_table(
|
|
||||||
jobs: List[str], results: List[CaseResult], class_: str
|
|
||||||
) -> ET.Element:
|
|
||||||
multiple_modules = len({r.module_name for r in results}) > 1
|
|
||||||
results_by_module_and_class = group_by(
|
|
||||||
results, lambda r: (r.module_name, r.class_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
job_row = HTML.tr(
|
|
||||||
HTML.th(), # column of case name
|
|
||||||
[HTML.th(HTML.div(HTML.span(job)), class_="job-name") for job in jobs],
|
|
||||||
)
|
|
||||||
|
|
||||||
rows = []
|
|
||||||
|
|
||||||
for (module_name, class_name), class_results in sorted(
|
|
||||||
results_by_module_and_class.items()
|
|
||||||
):
|
|
||||||
if multiple_modules:
|
|
||||||
# if the page shows classes from various modules, use the fully-qualified
|
|
||||||
# name in order to disambiguate and be clearer (eg. show
|
|
||||||
# "irctest.server_tests.extended_join.MetadataTestCase" instead of just
|
|
||||||
# "MetadataTestCase" which looks like it's about IRCv3's METADATA spec.
|
|
||||||
qualified_class_name = f"{module_name}.{class_name}"
|
|
||||||
else:
|
|
||||||
# otherwise, it's not needed, so let's not display it
|
|
||||||
qualified_class_name = class_name
|
|
||||||
|
|
||||||
module = importlib.import_module(module_name)
|
|
||||||
|
|
||||||
# Header row: class name
|
|
||||||
row_anchor = f"{qualified_class_name}"
|
|
||||||
rows.append(
|
|
||||||
HTML.tr(
|
|
||||||
HTML.th(
|
|
||||||
HTML.h2(
|
|
||||||
HTML.a(
|
|
||||||
qualified_class_name,
|
|
||||||
href=f"#{row_anchor}",
|
|
||||||
id=row_anchor,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
docstring(getattr(module, class_name)),
|
|
||||||
colspan=str(len(jobs) + 1),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Header row: one column for each implementation
|
|
||||||
rows.append(job_row)
|
|
||||||
|
|
||||||
# One row for each test:
|
|
||||||
results_by_test = group_by(class_results, key=lambda r: r.test_name)
|
|
||||||
for test_name, test_results in sorted(results_by_test.items()):
|
|
||||||
row_anchor = f"{qualified_class_name}.{test_name}"
|
|
||||||
if len(row_anchor) >= 50:
|
|
||||||
# Too long; give up on generating readable URL
|
|
||||||
# TODO: only hash test parameter
|
|
||||||
row_anchor = md5sum(row_anchor)
|
|
||||||
|
|
||||||
row = HTML.tr(
|
|
||||||
HTML.th(HTML.a(test_name, href=f"#{row_anchor}"), class_="test-name"),
|
|
||||||
id=row_anchor,
|
|
||||||
)
|
|
||||||
rows.append(row)
|
|
||||||
|
|
||||||
results_by_job = group_by(test_results, key=lambda r: r.job)
|
|
||||||
for job_name in jobs:
|
|
||||||
try:
|
|
||||||
(result,) = results_by_job[job_name]
|
|
||||||
except KeyError:
|
|
||||||
row.append(HTML.td("d", class_="deselected"))
|
|
||||||
continue
|
|
||||||
|
|
||||||
text: Union[str, None, ET.Element]
|
|
||||||
attrib = {}
|
|
||||||
|
|
||||||
if result.skipped:
|
|
||||||
attrib["class"] = "skipped"
|
|
||||||
if result.type == "pytest.skip":
|
|
||||||
text = "s"
|
|
||||||
elif result.type == "pytest.xfail":
|
|
||||||
text = "X"
|
|
||||||
attrib["class"] = "expected-failure"
|
|
||||||
else:
|
|
||||||
text = result.type
|
|
||||||
elif result.success:
|
|
||||||
attrib["class"] = "success"
|
|
||||||
if result.type:
|
|
||||||
# dead code?
|
|
||||||
text = result.type
|
|
||||||
else:
|
|
||||||
text = "."
|
|
||||||
else:
|
|
||||||
attrib["class"] = "failure"
|
|
||||||
if result.type:
|
|
||||||
# dead code?
|
|
||||||
text = result.type
|
|
||||||
else:
|
|
||||||
text = "f"
|
|
||||||
|
|
||||||
if result.system_out:
|
|
||||||
# There is a log file; link to it.
|
|
||||||
text = HTML.a(text or "?", href=f"./{result.output_filename()}")
|
|
||||||
else:
|
|
||||||
text = text or "?"
|
|
||||||
if result.message:
|
|
||||||
attrib["title"] = result.message
|
|
||||||
|
|
||||||
row.append(HTML.td(text, attrib))
|
|
||||||
|
|
||||||
return HTML.table(*rows, class_=class_)
|
|
||||||
|
|
||||||
|
|
||||||
def write_html_pages(
|
|
||||||
output_dir: Path, results: List[CaseResult]
|
|
||||||
) -> List[Tuple[str, str, str]]:
|
|
||||||
"""Returns the list of (module_name, file_name)."""
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
results_by_module = group_by(results, lambda r: r.module_name)
|
|
||||||
|
|
||||||
# used as columns
|
|
||||||
jobs = list(sorted({r.job for r in results}))
|
|
||||||
|
|
||||||
job_categories = {}
|
|
||||||
for job in jobs:
|
|
||||||
is_client = any(
|
|
||||||
"client_tests" in result.module_name and result.job == job
|
|
||||||
for result in results
|
|
||||||
)
|
|
||||||
is_server = any(
|
|
||||||
"server_tests" in result.module_name and result.job == job
|
|
||||||
for result in results
|
|
||||||
)
|
|
||||||
assert is_client != is_server, (job, is_client, is_server)
|
|
||||||
if job.endswith(("-atheme", "-anope", "-dlk")):
|
|
||||||
assert is_server
|
|
||||||
job_categories[job] = "server-with-services"
|
|
||||||
elif is_server:
|
|
||||||
job_categories[job] = "server" # with or without services
|
|
||||||
else:
|
|
||||||
assert is_client
|
|
||||||
job_categories[job] = "client"
|
|
||||||
|
|
||||||
pages = []
|
|
||||||
|
|
||||||
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]
|
|
||||||
for result in results
|
|
||||||
if result.module_name == module_name and not result.skipped
|
|
||||||
}
|
|
||||||
|
|
||||||
module_jobs = [job for job in jobs if job_categories[job] in module_categories]
|
|
||||||
|
|
||||||
root = build_module_html(module_jobs, module_results, module_name)
|
|
||||||
file_name = f"{module_name}.xhtml"
|
|
||||||
write_xml_file(output_dir / file_name, root)
|
|
||||||
pages.append(("module", module_name, file_name))
|
|
||||||
|
|
||||||
for category in ("server", "client"):
|
|
||||||
for job in [job for job in job_categories if job_categories[job] == category]:
|
|
||||||
job_results = [
|
|
||||||
result
|
|
||||||
for result in results
|
|
||||||
if result.job == job or result.job.startswith(job + "-")
|
|
||||||
]
|
|
||||||
root = build_job_html(job, job_results)
|
|
||||||
file_name = f"{job}.xhtml"
|
|
||||||
write_xml_file(output_dir / file_name, root)
|
|
||||||
pages.append(("job", job, file_name))
|
|
||||||
|
|
||||||
return pages
|
|
||||||
|
|
||||||
|
|
||||||
def write_test_outputs(output_dir: Path, results: List[CaseResult]) -> None:
|
|
||||||
"""Writes stdout files of each test."""
|
|
||||||
for result in results:
|
|
||||||
if result.system_out is None:
|
|
||||||
continue
|
|
||||||
output_file = output_dir / result.output_filename()
|
|
||||||
with output_file.open("wt") as fd:
|
|
||||||
fd.write(result.system_out)
|
|
||||||
|
|
||||||
|
|
||||||
def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> None:
|
|
||||||
module_pages = []
|
|
||||||
job_pages = []
|
|
||||||
for page_type, title, file_name in sorted(pages):
|
|
||||||
if page_type == "module":
|
|
||||||
module_pages.append((title, file_name))
|
|
||||||
elif page_type == "job":
|
|
||||||
job_pages.append((title, file_name))
|
|
||||||
else:
|
|
||||||
assert False, page_type
|
|
||||||
|
|
||||||
page = HTML.html(
|
|
||||||
HTML.head(
|
|
||||||
HTML.title("irctest dashboard"),
|
|
||||||
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
|
|
||||||
),
|
|
||||||
HTML.body(
|
|
||||||
HTML.h1("irctest dashboard"),
|
|
||||||
HTML.h2("Tests by command/specification"),
|
|
||||||
HTML.dl(
|
|
||||||
[
|
|
||||||
(
|
|
||||||
HTML.dt(HTML.a(module_name, href=f"./{file_name}")),
|
|
||||||
HTML.dd(docstring(importlib.import_module(module_name))),
|
|
||||||
)
|
|
||||||
for module_name, file_name in sorted(module_pages)
|
|
||||||
],
|
|
||||||
class_="module-index",
|
|
||||||
),
|
|
||||||
HTML.h2("Tests by implementation"),
|
|
||||||
HTML.ul(
|
|
||||||
[
|
|
||||||
HTML.li(HTML.a(job, href=f"./{file_name}"))
|
|
||||||
for job, file_name in sorted(job_pages)
|
|
||||||
],
|
|
||||||
class_="job-index",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
write_xml_file(output_dir / "index.xhtml", page)
|
|
||||||
|
|
||||||
|
|
||||||
def write_assets(output_dir: Path) -> None:
|
|
||||||
css_path = output_dir / "style.css"
|
|
||||||
source_css_path = Path(__file__).parent / "style.css"
|
|
||||||
with css_path.open("wt") as fd:
|
|
||||||
with source_css_path.open() as source_fd:
|
|
||||||
fd.write(source_fd.read())
|
|
||||||
|
|
||||||
|
|
||||||
def write_xml_file(filename: Path, root: ET.Element) -> None:
|
|
||||||
# Serialize
|
|
||||||
if sys.version_info >= (3, 8):
|
|
||||||
s = ET.tostring(root, default_namespace=HTML.uri)
|
|
||||||
else:
|
|
||||||
# default_namespace not supported
|
|
||||||
s = ET.tostring(root)
|
|
||||||
|
|
||||||
with filename.open("wb") as fd:
|
|
||||||
fd.write(s)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_xml_file(filename: Path) -> ET.ElementTree:
|
|
||||||
fd: IO
|
|
||||||
if filename.suffix == ".gz":
|
|
||||||
with gzip.open(filename, "rb") as fd: # type: ignore
|
|
||||||
return parse_xml(fd) # type: ignore
|
|
||||||
else:
|
|
||||||
with open(filename) as fd:
|
|
||||||
return parse_xml(fd) # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
def main(output_path: Path, filenames: List[Path]) -> int:
|
|
||||||
results = [
|
|
||||||
result
|
|
||||||
for filename in filenames
|
|
||||||
for result in iter_job_results(filename, parse_xml_file(filename))
|
|
||||||
]
|
|
||||||
|
|
||||||
pages = write_html_pages(output_path, results)
|
|
||||||
|
|
||||||
write_html_index(output_path, pages)
|
|
||||||
write_test_outputs(output_path, results)
|
|
||||||
write_assets(output_path)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
(_, output_path, *filenames) = sys.argv
|
|
||||||
exit(main(Path(output_path), list(map(Path, filenames))))
|
|
@ -1,87 +0,0 @@
|
|||||||
import dataclasses
|
|
||||||
import gzip
|
|
||||||
import io
|
|
||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
import sys
|
|
||||||
from typing import Iterator
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
import zipfile
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class Artifact:
|
|
||||||
repo: str
|
|
||||||
run_id: int
|
|
||||||
name: str
|
|
||||||
download_url: str
|
|
||||||
|
|
||||||
@property
|
|
||||||
def public_download_url(self) -> str:
|
|
||||||
# GitHub API is not available publicly for artifacts, we need to use
|
|
||||||
# a third-party proxy to access it...
|
|
||||||
name = urllib.parse.quote(self.name)
|
|
||||||
return f"https://nightly.link/{repo}/actions/runs/{self.run_id}/{name}.zip"
|
|
||||||
|
|
||||||
|
|
||||||
def iter_run_artifacts(repo: str, run_id: int) -> Iterator[Artifact]:
|
|
||||||
request = urllib.request.Request(
|
|
||||||
f"https://api.github.com/repos/{repo}/actions/runs/{run_id}/artifacts"
|
|
||||||
"?per_page=100",
|
|
||||||
headers={"Accept": "application/vnd.github.v3+json"},
|
|
||||||
)
|
|
||||||
|
|
||||||
response = urllib.request.urlopen(request)
|
|
||||||
|
|
||||||
for artifact in json.load(response)["artifacts"]:
|
|
||||||
if not artifact["name"].startswith(("pytest-results_", "pytest results ")):
|
|
||||||
continue
|
|
||||||
if artifact["expired"]:
|
|
||||||
continue
|
|
||||||
yield Artifact(
|
|
||||||
repo=repo,
|
|
||||||
run_id=run_id,
|
|
||||||
name=artifact["name"],
|
|
||||||
download_url=artifact["archive_download_url"],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def download_artifact(output_name: Path, url: str) -> None:
|
|
||||||
if output_name.exists():
|
|
||||||
return
|
|
||||||
response = urllib.request.urlopen(url)
|
|
||||||
archive_bytes = response.read() # Can't stream it, it's a ZIP
|
|
||||||
with zipfile.ZipFile(io.BytesIO(archive_bytes)) as archive:
|
|
||||||
with archive.open("pytest.xml") as input_fd:
|
|
||||||
pytest_xml = input_fd.read()
|
|
||||||
|
|
||||||
tmp_output_path = output_name.with_suffix(".tmp")
|
|
||||||
with gzip.open(tmp_output_path, "wb") as output_fd:
|
|
||||||
output_fd.write(pytest_xml)
|
|
||||||
|
|
||||||
# Atomically write to the output path, so that we don't write partial files in case
|
|
||||||
# the download process is interrupted
|
|
||||||
tmp_output_path.rename(output_name)
|
|
||||||
|
|
||||||
|
|
||||||
def main(output_dir: Path, repo: str, run_id: int) -> int:
|
|
||||||
output_dir.mkdir(parents=True, exist_ok=True)
|
|
||||||
run_path = output_dir / str(run_id)
|
|
||||||
run_path.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
for artifact in iter_run_artifacts(repo, run_id):
|
|
||||||
artifact_path = run_path / artifact.name / "pytest.xml.gz"
|
|
||||||
artifact_path.parent.mkdir(exist_ok=True)
|
|
||||||
try:
|
|
||||||
download_artifact(artifact_path, artifact.download_url)
|
|
||||||
except Exception:
|
|
||||||
download_artifact(artifact_path, artifact.public_download_url)
|
|
||||||
print("downloaded", artifact.name)
|
|
||||||
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
(_, output_path, repo, run_id) = sys.argv
|
|
||||||
exit(main(Path(output_path), repo, int(run_id)))
|
|
@ -1,126 +0,0 @@
|
|||||||
# Copyright (c) 2023 Valentin Lorentz
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in all
|
|
||||||
# copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
# SOFTWARE.
|
|
||||||
|
|
||||||
"""This module allows writing XML ASTs in a way that is more concise than the default
|
|
||||||
:mod:`xml.etree.ElementTree` interface.
|
|
||||||
|
|
||||||
For example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from .shortxml import Namespace
|
|
||||||
|
|
||||||
HTML = Namespace("http://www.w3.org/1999/xhtml")
|
|
||||||
|
|
||||||
page = HTML.html(
|
|
||||||
HTML.head(
|
|
||||||
HTML.title("irctest dashboard"),
|
|
||||||
HTML.link(rel="stylesheet", type="text/css", href="./style.css"),
|
|
||||||
),
|
|
||||||
HTML.body(
|
|
||||||
HTML.h1("irctest dashboard"),
|
|
||||||
HTML.h2("Tests by command/specification"),
|
|
||||||
HTML.dl(
|
|
||||||
[
|
|
||||||
( # elements can be arbitrarily nested in lists
|
|
||||||
HTML.dt(HTML.a(title, href=f"./{title}.xhtml")),
|
|
||||||
HTML.dd(defintion),
|
|
||||||
)
|
|
||||||
for title, definition in sorted(definitions)
|
|
||||||
],
|
|
||||||
class_="module-index",
|
|
||||||
),
|
|
||||||
HTML.h2("Tests by implementation"),
|
|
||||||
HTML.ul(
|
|
||||||
[
|
|
||||||
HTML.li(HTML.a(job, href=f"./{file_name}"))
|
|
||||||
for job, file_name in sorted(job_pages)
|
|
||||||
],
|
|
||||||
class_="job-index",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
print(ET.tostring(page, default_namespace=HTML.uri))
|
|
||||||
|
|
||||||
|
|
||||||
Attributes can be passed either as dictionaries or as kwargs, and can be mixed
|
|
||||||
with child elements.
|
|
||||||
Trailing underscores are stripped from attributes, which allows passing reserved
|
|
||||||
Python keywords (eg. ``class_`` instead of ``class``)
|
|
||||||
|
|
||||||
Attributes are always qualified, and share the namespace of the element they are
|
|
||||||
attached to.
|
|
||||||
|
|
||||||
Mixed content (elements containing both text and child elements) is not supported.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Dict, Sequence, Union
|
|
||||||
import xml.etree.ElementTree as ET
|
|
||||||
|
|
||||||
|
|
||||||
def _namespacify(ns: str, s: str) -> str:
|
|
||||||
return "{%s}%s" % (ns, s)
|
|
||||||
|
|
||||||
|
|
||||||
_Children = Union[None, Dict[str, str], ET.Element, Sequence["_Children"]]
|
|
||||||
|
|
||||||
|
|
||||||
class ElementFactory:
|
|
||||||
def __init__(self, namespace: str, tag: str):
|
|
||||||
self._tag = _namespacify(namespace, tag)
|
|
||||||
self._namespace = namespace
|
|
||||||
|
|
||||||
def __call__(self, *args: Union[str, _Children], **kwargs: str) -> ET.Element:
|
|
||||||
e = ET.Element(self._tag)
|
|
||||||
|
|
||||||
attributes = {k.rstrip("_"): v for (k, v) in kwargs.items()}
|
|
||||||
children = [*args, attributes]
|
|
||||||
|
|
||||||
if args and isinstance(children[0], str):
|
|
||||||
e.text = children[0]
|
|
||||||
children.pop(0)
|
|
||||||
|
|
||||||
for child in children:
|
|
||||||
self._append_child(e, child)
|
|
||||||
|
|
||||||
return e
|
|
||||||
|
|
||||||
def _append_child(self, e: ET.Element, child: _Children) -> None:
|
|
||||||
if isinstance(child, ET.Element):
|
|
||||||
e.append(child)
|
|
||||||
elif child is None:
|
|
||||||
pass
|
|
||||||
elif isinstance(child, dict):
|
|
||||||
for k, v in child.items():
|
|
||||||
e.set(_namespacify(self._namespace, k), str(v))
|
|
||||||
elif isinstance(child, str):
|
|
||||||
raise ValueError("Mixed content is not supported")
|
|
||||||
else:
|
|
||||||
for grandchild in child:
|
|
||||||
self._append_child(e, grandchild)
|
|
||||||
|
|
||||||
|
|
||||||
class Namespace:
|
|
||||||
def __init__(self, uri: str):
|
|
||||||
self.uri = uri
|
|
||||||
|
|
||||||
def __getattr__(self, tag: str) -> ElementFactory:
|
|
||||||
return ElementFactory(self.uri, tag)
|
|
@ -1,67 +0,0 @@
|
|||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background-color: #121212;
|
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
filter: invert(0.85) hue-rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dl.module-index {
|
|
||||||
column-width: 40em; /* Magic constant for 2 columns on average laptop/desktop */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Only 1px solid border between cells */
|
|
||||||
table.test-matrix {
|
|
||||||
border-spacing: 0;
|
|
||||||
border-collapse: collapse;
|
|
||||||
}
|
|
||||||
table.test-matrix td {
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid grey;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make link take the whole cell */
|
|
||||||
table.test-matrix td a {
|
|
||||||
display: block;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
color: black;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Test matrix colors */
|
|
||||||
table.test-matrix .deselected {
|
|
||||||
background-color: grey;
|
|
||||||
}
|
|
||||||
table.test-matrix .success {
|
|
||||||
background-color: green;
|
|
||||||
}
|
|
||||||
table.test-matrix .skipped {
|
|
||||||
background-color: yellow;
|
|
||||||
}
|
|
||||||
table.test-matrix .failure {
|
|
||||||
background-color: red;
|
|
||||||
}
|
|
||||||
table.test-matrix .expected-failure {
|
|
||||||
background-color: orange;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rotate headers, thanks to https://css-tricks.com/rotated-table-column-headers/ */
|
|
||||||
table.module-results th.job-name {
|
|
||||||
height: 140px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
table.module-results th.job-name > div {
|
|
||||||
transform:
|
|
||||||
translate(28px, 50px)
|
|
||||||
rotate(315deg);
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
table.module-results th.job-name > div > span {
|
|
||||||
border-bottom: 1px solid grey;
|
|
||||||
padding-left: 0px;
|
|
||||||
}
|
|
@ -66,7 +66,6 @@ RPL_WHOISIDLE = "317"
|
|||||||
RPL_ENDOFWHOIS = "318"
|
RPL_ENDOFWHOIS = "318"
|
||||||
RPL_WHOISCHANNELS = "319"
|
RPL_WHOISCHANNELS = "319"
|
||||||
RPL_WHOISSPECIAL = "320"
|
RPL_WHOISSPECIAL = "320"
|
||||||
RPL_LISTSTART = "321"
|
|
||||||
RPL_LIST = "322"
|
RPL_LIST = "322"
|
||||||
RPL_LISTEND = "323"
|
RPL_LISTEND = "323"
|
||||||
RPL_CHANNELMODEIS = "324"
|
RPL_CHANNELMODEIS = "324"
|
||||||
@ -87,7 +86,6 @@ RPL_ENDOFEXCEPTLIST = "349"
|
|||||||
RPL_VERSION = "351"
|
RPL_VERSION = "351"
|
||||||
RPL_WHOREPLY = "352"
|
RPL_WHOREPLY = "352"
|
||||||
RPL_NAMREPLY = "353"
|
RPL_NAMREPLY = "353"
|
||||||
RPL_WHOSPCRPL = "354"
|
|
||||||
RPL_LINKS = "364"
|
RPL_LINKS = "364"
|
||||||
RPL_ENDOFLINKS = "365"
|
RPL_ENDOFLINKS = "365"
|
||||||
RPL_ENDOFNAMES = "366"
|
RPL_ENDOFNAMES = "366"
|
||||||
@ -142,7 +140,6 @@ ERR_USERONCHANNEL = "443"
|
|||||||
ERR_NOLOGIN = "444"
|
ERR_NOLOGIN = "444"
|
||||||
ERR_SUMMONDISABLED = "445"
|
ERR_SUMMONDISABLED = "445"
|
||||||
ERR_USERSDISABLED = "446"
|
ERR_USERSDISABLED = "446"
|
||||||
ERR_FORBIDDENCHANNEL = "448"
|
|
||||||
ERR_NOTREGISTERED = "451"
|
ERR_NOTREGISTERED = "451"
|
||||||
ERR_NEEDMOREPARAMS = "461"
|
ERR_NEEDMOREPARAMS = "461"
|
||||||
ERR_ALREADYREGISTRED = "462"
|
ERR_ALREADYREGISTRED = "462"
|
||||||
|
@ -13,18 +13,18 @@ class Operator:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class _AnyStr(Operator):
|
class AnyStr(Operator):
|
||||||
"""Wildcard matching any string"""
|
"""Wildcard matching any string"""
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "ANYSTR"
|
return "AnyStr"
|
||||||
|
|
||||||
|
|
||||||
class _AnyOptStr(Operator):
|
class AnyOptStr(Operator):
|
||||||
"""Wildcard matching any string as well as None"""
|
"""Wildcard matching any string as well as None"""
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return "ANYOPTSTR"
|
return "AnyOptStr"
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
@ -43,14 +43,6 @@ class NotStrRe(Operator):
|
|||||||
return f"NotStrRe(r'{self.regexp}')"
|
return f"NotStrRe(r'{self.regexp}')"
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
|
||||||
class InsensitiveStr(Operator):
|
|
||||||
string: str
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
|
||||||
return f"InsensitiveStr({self.string!r})"
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
@dataclasses.dataclass(frozen=True)
|
||||||
class RemainingKeys(Operator):
|
class RemainingKeys(Operator):
|
||||||
"""Used in a dict pattern to match all remaining keys.
|
"""Used in a dict pattern to match all remaining keys.
|
||||||
@ -59,42 +51,30 @@ class RemainingKeys(Operator):
|
|||||||
key: Operator
|
key: Operator
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"RemainingKeys({self.key!r})"
|
return f"Keys({self.key!r})"
|
||||||
|
|
||||||
|
|
||||||
ANYSTR = _AnyStr()
|
ANYSTR = AnyStr()
|
||||||
"""Singleton, spares two characters"""
|
"""Singleton, spares two characters"""
|
||||||
|
|
||||||
ANYOPTSTR = _AnyOptStr()
|
ANYDICT = {RemainingKeys(ANYSTR): AnyOptStr()}
|
||||||
"""Singleton, spares two characters"""
|
|
||||||
|
|
||||||
ANYDICT = {RemainingKeys(ANYSTR): ANYOPTSTR}
|
|
||||||
"""Matches any dictionary; useful to compare tags dict, eg.
|
"""Matches any dictionary; useful to compare tags dict, eg.
|
||||||
`match_dict(got_tags, {"label": "foo", **ANYDICT})`"""
|
`match_dict(got_tags, {"label": "foo", **ANYDICT})`"""
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
class _AnyListRemainder:
|
||||||
class ListRemainder:
|
|
||||||
item: Operator
|
|
||||||
min_length: int = 0
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
if self.min_length:
|
return "*ANYLIST"
|
||||||
return f"ListRemainder({self.item!r}, min_length={self.min_length})"
|
|
||||||
elif self.item is ANYSTR:
|
|
||||||
return "*ANYLIST"
|
|
||||||
else:
|
|
||||||
return f"ListRemainder({self.item!r})"
|
|
||||||
|
|
||||||
|
|
||||||
ANYLIST = [ListRemainder(ANYSTR)]
|
ANYLIST = [_AnyListRemainder()]
|
||||||
"""Matches any list remainder"""
|
"""Matches any list remainder"""
|
||||||
|
|
||||||
|
|
||||||
def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bool:
|
def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bool:
|
||||||
if isinstance(expected, _AnyOptStr):
|
if isinstance(expected, AnyOptStr):
|
||||||
return True
|
return True
|
||||||
elif isinstance(expected, _AnyStr) and got is not None:
|
elif isinstance(expected, AnyStr) and got is not None:
|
||||||
return True
|
return True
|
||||||
elif isinstance(expected, StrRe):
|
elif isinstance(expected, StrRe):
|
||||||
if got is None or not re.match(expected.regexp, got):
|
if got is None or not re.match(expected.regexp, got):
|
||||||
@ -102,9 +82,6 @@ def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bo
|
|||||||
elif isinstance(expected, NotStrRe):
|
elif isinstance(expected, NotStrRe):
|
||||||
if got is None or re.match(expected.regexp, got):
|
if got is None or re.match(expected.regexp, got):
|
||||||
return False
|
return False
|
||||||
elif isinstance(expected, InsensitiveStr):
|
|
||||||
if got is None or got.lower() != expected.string.lower():
|
|
||||||
return False
|
|
||||||
elif isinstance(expected, Operator):
|
elif isinstance(expected, Operator):
|
||||||
raise NotImplementedError(f"Unsupported operator: {expected}")
|
raise NotImplementedError(f"Unsupported operator: {expected}")
|
||||||
elif got != expected:
|
elif got != expected:
|
||||||
@ -121,13 +98,9 @@ def match_list(
|
|||||||
The ANYSTR operator can be used on the 'expected' side as a wildcard,
|
The ANYSTR operator can be used on the 'expected' side as a wildcard,
|
||||||
matching any *single* value; and StrRe("<regexp>") can be used to match regular
|
matching any *single* value; and StrRe("<regexp>") can be used to match regular
|
||||||
expressions"""
|
expressions"""
|
||||||
if expected and isinstance(expected[-1], ListRemainder):
|
if expected[-1] is ANYLIST[0]:
|
||||||
# Expand the 'expected' list to have as many items as the 'got' list
|
expected = expected[0:-1]
|
||||||
expected = list(expected) # copy
|
got = got[0 : len(expected)] # Ignore remaining
|
||||||
remainder = expected.pop()
|
|
||||||
nb_remaining_items = len(got) - len(expected)
|
|
||||||
expected += [remainder.item] * max(nb_remaining_items, remainder.min_length)
|
|
||||||
|
|
||||||
if len(got) != len(expected):
|
if len(got) != len(expected):
|
||||||
return False
|
return False
|
||||||
return all(
|
return all(
|
||||||
@ -152,23 +125,21 @@ def match_dict(
|
|||||||
# Set to not-None if we find a Keys() operator in the dict keys
|
# Set to not-None if we find a Keys() operator in the dict keys
|
||||||
remaining_keys_wildcard = None
|
remaining_keys_wildcard = None
|
||||||
|
|
||||||
for expected_key, expected_value in expected.items():
|
for (expected_key, expected_value) in expected.items():
|
||||||
if isinstance(expected_key, RemainingKeys):
|
if isinstance(expected_key, RemainingKeys):
|
||||||
remaining_keys_wildcard = (expected_key.key, expected_value)
|
remaining_keys_wildcard = (expected_key.key, expected_value)
|
||||||
|
elif isinstance(expected_key, Operator):
|
||||||
|
raise NotImplementedError(f"Unsupported operator: {expected_key}")
|
||||||
else:
|
else:
|
||||||
for key in got:
|
if expected_key not in got:
|
||||||
if match_string(key, expected_key) and match_string(
|
return False
|
||||||
got[key], expected_value
|
got_value = got.pop(expected_key)
|
||||||
):
|
if not match_string(got_value, expected_value):
|
||||||
got.pop(key)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
# Found no (key, value) pair matching the request
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if remaining_keys_wildcard:
|
if remaining_keys_wildcard:
|
||||||
(expected_key, expected_value) = remaining_keys_wildcard
|
(expected_key, expected_value) = remaining_keys_wildcard
|
||||||
for key, value in got.items():
|
for (key, value) in got.items():
|
||||||
if not match_string(key, expected_key):
|
if not match_string(key, expected_key):
|
||||||
return False
|
return False
|
||||||
if not match_string(value, expected_value):
|
if not match_string(value, expected_value):
|
||||||
|
@ -14,11 +14,6 @@ class ImplementationChoice(unittest.SkipTest):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class OptionalCommandNotSupported(unittest.SkipTest):
|
|
||||||
def __str__(self) -> str:
|
|
||||||
return "Unsupported command: {}".format(self.args[0])
|
|
||||||
|
|
||||||
|
|
||||||
class OptionalExtensionNotSupported(unittest.SkipTest):
|
class OptionalExtensionNotSupported(unittest.SkipTest):
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return "Unsupported extension: {}".format(self.args[0])
|
return "Unsupported extension: {}".format(self.args[0])
|
||||||
|
@ -1,24 +1,13 @@
|
|||||||
"""Internal checks of assertion implementations."""
|
|
||||||
|
|
||||||
from typing import Dict, List, Tuple
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.irc_utils.message_parser import parse_message
|
from irctest.irc_utils.message_parser import parse_message
|
||||||
from irctest.patma import (
|
from irctest.patma import ANYDICT, ANYSTR, AnyOptStr, NotStrRe, RemainingKeys, StrRe
|
||||||
ANYDICT,
|
|
||||||
ANYLIST,
|
|
||||||
ANYOPTSTR,
|
|
||||||
ANYSTR,
|
|
||||||
ListRemainder,
|
|
||||||
NotStrRe,
|
|
||||||
RemainingKeys,
|
|
||||||
StrRe,
|
|
||||||
)
|
|
||||||
|
|
||||||
# fmt: off
|
# fmt: off
|
||||||
MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [
|
||||||
(
|
(
|
||||||
# the specification:
|
# the specification:
|
||||||
dict(
|
dict(
|
||||||
@ -38,11 +27,6 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
[
|
[
|
||||||
"PRIVMSG #chan hello2",
|
"PRIVMSG #chan hello2",
|
||||||
"PRIVMSG #chan2 hello",
|
"PRIVMSG #chan2 hello",
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']",
|
|
||||||
"expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']",
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -65,11 +49,6 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
[
|
[
|
||||||
"PRIVMSG #chan :hi",
|
"PRIVMSG #chan :hi",
|
||||||
"PRIVMSG #chan2 hello",
|
"PRIVMSG #chan2 hello",
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected params to match ['#chan', StrRe(r'hello.*')], got ['#chan', 'hi']",
|
|
||||||
"expected params to match ['#chan', StrRe(r'hello.*')], got ['#chan2', 'hello']",
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -88,12 +67,6 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
"PRIVMSG #chan :hi",
|
"PRIVMSG #chan :hi",
|
||||||
":foo2!baz@qux PRIVMSG #chan hello",
|
":foo2!baz@qux PRIVMSG #chan hello",
|
||||||
"@tag1=bar :foo2!baz@qux PRIVMSG #chan :hello",
|
"@tag1=bar :foo2!baz@qux PRIVMSG #chan :hello",
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected nick to be foo, got None instead",
|
|
||||||
"expected nick to be foo, got foo2 instead",
|
|
||||||
"expected nick to be foo, got foo2 instead",
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -114,13 +87,6 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
"@tag1=value1 PRIVMSG #chan :hello",
|
"@tag1=value1 PRIVMSG #chan :hello",
|
||||||
"PRIVMSG #chan hello",
|
"PRIVMSG #chan hello",
|
||||||
":foo!baz@qux PRIVMSG #chan hello",
|
":foo!baz@qux PRIVMSG #chan hello",
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected tags to match {'tag1': 'bar'}, got {'tag1': 'bar', 'tag2': ''}",
|
|
||||||
"expected tags to match {'tag1': 'bar'}, got {'tag1': 'value1'}",
|
|
||||||
"expected tags to match {'tag1': 'bar'}, got {}",
|
|
||||||
"expected tags to match {'tag1': 'bar'}, got {}",
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -141,12 +107,6 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
"@tag1=bar;tag2= PRIVMSG #chan :hello",
|
"@tag1=bar;tag2= PRIVMSG #chan :hello",
|
||||||
"PRIVMSG #chan hello",
|
"PRIVMSG #chan hello",
|
||||||
":foo!baz@qux PRIVMSG #chan hello",
|
":foo!baz@qux PRIVMSG #chan hello",
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected tags to match {'tag1': ANYSTR}, got {'tag1': 'bar', 'tag2': ''}",
|
|
||||||
"expected tags to match {'tag1': ANYSTR}, got {}",
|
|
||||||
"expected tags to match {'tag1': ANYSTR}, got {}",
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -169,53 +129,12 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
"PRIVMSG #chan hello2",
|
"PRIVMSG #chan hello2",
|
||||||
"PRIVMSG #chan2 hello",
|
"PRIVMSG #chan2 hello",
|
||||||
":foo!baz@qux PRIVMSG #chan hello",
|
":foo!baz@qux PRIVMSG #chan hello",
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected command to be PRIVMSG, got PRIVMG",
|
|
||||||
"expected tags to match {'tag1': 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {'tag1': 'value1'}",
|
|
||||||
"expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']",
|
|
||||||
"expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']",
|
|
||||||
"expected tags to match {'tag1': 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {}",
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
# the specification:
|
# the specification:
|
||||||
dict(
|
dict(
|
||||||
tags={StrRe("tag[12]"): "bar", **ANYDICT},
|
tags={"tag1": "bar", RemainingKeys(NotStrRe("tag2")): AnyOptStr()},
|
||||||
command="PRIVMSG",
|
|
||||||
params=["#chan", "hello"],
|
|
||||||
),
|
|
||||||
# matches:
|
|
||||||
[
|
|
||||||
"@tag1=bar PRIVMSG #chan :hello",
|
|
||||||
"@tag1=bar;tag2= PRIVMSG #chan :hello",
|
|
||||||
"@tag1=bar :foo!baz@qux PRIVMSG #chan :hello",
|
|
||||||
"@tag2=bar PRIVMSG #chan :hello",
|
|
||||||
"@tag1=bar;tag2= PRIVMSG #chan :hello",
|
|
||||||
"@tag1=;tag2=bar PRIVMSG #chan :hello",
|
|
||||||
],
|
|
||||||
# and does not match:
|
|
||||||
[
|
|
||||||
"PRIVMG #chan :hello",
|
|
||||||
"@tag1=value1 PRIVMSG #chan :hello",
|
|
||||||
"PRIVMSG #chan hello2",
|
|
||||||
"PRIVMSG #chan2 hello",
|
|
||||||
":foo!baz@qux PRIVMSG #chan hello",
|
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected command to be PRIVMSG, got PRIVMG",
|
|
||||||
"expected tags to match {StrRe(r'tag[12]'): 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {'tag1': 'value1'}",
|
|
||||||
"expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']",
|
|
||||||
"expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']",
|
|
||||||
"expected tags to match {StrRe(r'tag[12]'): 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {}",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# the specification:
|
|
||||||
dict(
|
|
||||||
tags={"tag1": "bar", RemainingKeys(NotStrRe("tag2")): ANYOPTSTR},
|
|
||||||
command="PRIVMSG",
|
command="PRIVMSG",
|
||||||
params=["#chan", "hello"],
|
params=["#chan", "hello"],
|
||||||
),
|
),
|
||||||
@ -231,98 +150,6 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [
|
|||||||
"@tag1=value1 PRIVMSG #chan :hello",
|
"@tag1=value1 PRIVMSG #chan :hello",
|
||||||
"@tag1=bar;tag2= PRIVMSG #chan :hello",
|
"@tag1=bar;tag2= PRIVMSG #chan :hello",
|
||||||
"@tag1=bar;tag2=baz PRIVMSG #chan :hello",
|
"@tag1=bar;tag2=baz PRIVMSG #chan :hello",
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected command to be PRIVMSG, got PRIVMG",
|
|
||||||
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'value1'}",
|
|
||||||
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'bar', 'tag2': ''}",
|
|
||||||
"expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'bar', 'tag2': 'baz'}",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# the specification:
|
|
||||||
dict(
|
|
||||||
command="005",
|
|
||||||
params=["nick", "FOO=1", *ANYLIST],
|
|
||||||
),
|
|
||||||
# matches:
|
|
||||||
[
|
|
||||||
"005 nick FOO=1",
|
|
||||||
"005 nick FOO=1 BAR=2",
|
|
||||||
],
|
|
||||||
# and does not match:
|
|
||||||
[
|
|
||||||
"005 nick",
|
|
||||||
"005 nick BAR=2",
|
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected params to match ['nick', 'FOO=1', *ANYLIST], got ['nick']",
|
|
||||||
"expected params to match ['nick', 'FOO=1', *ANYLIST], got ['nick', 'BAR=2']",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# the specification:
|
|
||||||
dict(
|
|
||||||
command="005",
|
|
||||||
params=["nick", ListRemainder(ANYSTR, min_length=1)],
|
|
||||||
),
|
|
||||||
# matches:
|
|
||||||
[
|
|
||||||
"005 nick FOO=1",
|
|
||||||
"005 nick FOO=1 BAR=2",
|
|
||||||
"005 nick BAR=2",
|
|
||||||
],
|
|
||||||
# and does not match:
|
|
||||||
[
|
|
||||||
"005 nick",
|
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected params to match ['nick', ListRemainder(ANYSTR, min_length=1)], got ['nick']",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# the specification:
|
|
||||||
dict(
|
|
||||||
command="005",
|
|
||||||
params=["nick", ListRemainder(StrRe("[A-Z]+=.*"), min_length=1)],
|
|
||||||
),
|
|
||||||
# matches:
|
|
||||||
[
|
|
||||||
"005 nick FOO=1",
|
|
||||||
"005 nick FOO=1 BAR=2",
|
|
||||||
"005 nick BAR=2",
|
|
||||||
],
|
|
||||||
# and does not match:
|
|
||||||
[
|
|
||||||
"005 nick",
|
|
||||||
"005 nick foo=1",
|
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected params to match ['nick', ListRemainder(StrRe(r'[A-Z]+=.*'), min_length=1)], got ['nick']",
|
|
||||||
"expected params to match ['nick', ListRemainder(StrRe(r'[A-Z]+=.*'), min_length=1)], got ['nick', 'foo=1']",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
(
|
|
||||||
# the specification:
|
|
||||||
dict(
|
|
||||||
command="PING",
|
|
||||||
params=["abc"]
|
|
||||||
),
|
|
||||||
# matches:
|
|
||||||
[
|
|
||||||
"PING abc",
|
|
||||||
],
|
|
||||||
# and does not match:
|
|
||||||
[
|
|
||||||
"PONG def"
|
|
||||||
],
|
|
||||||
# and they each error with:
|
|
||||||
[
|
|
||||||
"expected command to be PING, got PONG"
|
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
@ -334,7 +161,7 @@ class IrcTestCaseTestCase(cases._IrcTestCase):
|
|||||||
"spec,msg",
|
"spec,msg",
|
||||||
[
|
[
|
||||||
pytest.param(spec, msg, id=f"{spec}-{msg}")
|
pytest.param(spec, msg, id=f"{spec}-{msg}")
|
||||||
for (spec, positive_matches, _, _) in MESSAGE_SPECS
|
for (spec, positive_matches, _) in MESSAGE_SPECS
|
||||||
for msg in positive_matches
|
for msg in positive_matches
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -347,7 +174,7 @@ class IrcTestCaseTestCase(cases._IrcTestCase):
|
|||||||
"spec,msg",
|
"spec,msg",
|
||||||
[
|
[
|
||||||
pytest.param(spec, msg, id=f"{spec}-{msg}")
|
pytest.param(spec, msg, id=f"{spec}-{msg}")
|
||||||
for (spec, _, negative_matches, _) in MESSAGE_SPECS
|
for (spec, _, negative_matches) in MESSAGE_SPECS
|
||||||
for msg in negative_matches
|
for msg in negative_matches
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -356,14 +183,3 @@ class IrcTestCaseTestCase(cases._IrcTestCase):
|
|||||||
assert not self.messageEqual(parse_message(msg), **spec), msg
|
assert not self.messageEqual(parse_message(msg), **spec), msg
|
||||||
with pytest.raises(AssertionError):
|
with pytest.raises(AssertionError):
|
||||||
self.assertMessageMatch(parse_message(msg), **spec), msg
|
self.assertMessageMatch(parse_message(msg), **spec), msg
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"spec,msg,error_string",
|
|
||||||
[
|
|
||||||
pytest.param(spec, msg, error_string, id=error_string)
|
|
||||||
for (spec, _, negative_matches, error_stringgexps) in MESSAGE_SPECS
|
|
||||||
for (msg, error_string) in zip(negative_matches, error_stringgexps)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_message_matching_negative_message(self, spec, msg, error_string):
|
|
||||||
self.assertIn(error_string, self.messageDiffers(parse_message(msg), **spec))
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
`IRCv3 account-tag <https://ircv3.net/specs/extensions/account-tag>`_
|
<http://ircv3.net/specs/extensions/account-tag-3.2.html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
@cases.mark_services
|
||||||
class AccountTagTestCase(cases.BaseServerTestCase):
|
class AccountTagTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||||
def connectRegisteredClient(self, nick):
|
def connectRegisteredClient(self, nick):
|
||||||
self.addClient()
|
self.addClient()
|
||||||
self.sendLine(2, "CAP LS 302")
|
self.sendLine(2, "CAP LS 302")
|
||||||
@ -40,7 +40,7 @@ class AccountTagTestCase(cases.BaseServerTestCase):
|
|||||||
self.skipToWelcome(2)
|
self.skipToWelcome(2)
|
||||||
|
|
||||||
@cases.mark_capabilities("account-tag")
|
@cases.mark_capabilities("account-tag")
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
def testPrivmsg(self):
|
def testPrivmsg(self):
|
||||||
self.connectClient("foo", capabilities=["account-tag"], skip_if_cap_nak=True)
|
self.connectClient("foo", capabilities=["account-tag"], skip_if_cap_nak=True)
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
@ -54,10 +54,7 @@ class AccountTagTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_capabilities("account-tag")
|
@cases.mark_capabilities("account-tag")
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["Charybdis"], "https://github.com/solanum-ircd/solanum/issues/166"
|
|
||||||
)
|
|
||||||
def testInvite(self):
|
def testInvite(self):
|
||||||
self.connectClient("foo", capabilities=["account-tag"], skip_if_cap_nak=True)
|
self.connectClient("foo", capabilities=["account-tag"], skip_if_cap_nak=True)
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
|
@ -1,16 +1,5 @@
|
|||||||
"""
|
|
||||||
AWAY command (`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-4.1>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#away-message>`__)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import (
|
from irctest.numerics import RPL_AWAY, RPL_NOWAWAY, RPL_UNAWAY, RPL_USERHOST
|
||||||
RPL_AWAY,
|
|
||||||
RPL_NOWAWAY,
|
|
||||||
RPL_UNAWAY,
|
|
||||||
RPL_USERHOST,
|
|
||||||
RPL_WHOISUSER,
|
|
||||||
)
|
|
||||||
from irctest.patma import StrRe
|
from irctest.patma import StrRe
|
||||||
|
|
||||||
|
|
||||||
@ -43,7 +32,7 @@ class AwayTestCase(cases.BaseServerTestCase):
|
|||||||
"""
|
"""
|
||||||
"The server acknowledges the change in away status by returning the
|
"The server acknowledges the change in away status by returning the
|
||||||
`RPL_NOWAWAY` and `RPL_UNAWAY` numerics."
|
`RPL_NOWAWAY` and `RPL_UNAWAY` numerics."
|
||||||
-- https://modern.ircdocs.horse/#away-message
|
-- https://github.com/ircdocs/modern-irc/pull/100
|
||||||
"""
|
"""
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
self.sendLine(1, "AWAY :I'm not here right now")
|
self.sendLine(1, "AWAY :I'm not here right now")
|
||||||
@ -59,7 +48,7 @@ class AwayTestCase(cases.BaseServerTestCase):
|
|||||||
"""
|
"""
|
||||||
"Servers SHOULD notify clients when a user they're interacting with
|
"Servers SHOULD notify clients when a user they're interacting with
|
||||||
is away when relevant"
|
is away when relevant"
|
||||||
-- https://modern.ircdocs.horse/#away-message
|
-- https://github.com/ircdocs/modern-irc/pull/100
|
||||||
|
|
||||||
"<client> <nick> :<message>"
|
"<client> <nick> :<message>"
|
||||||
-- https://modern.ircdocs.horse/#rplaway-301
|
-- https://modern.ircdocs.horse/#rplaway-301
|
||||||
@ -86,7 +75,7 @@ class AwayTestCase(cases.BaseServerTestCase):
|
|||||||
"""
|
"""
|
||||||
"Servers SHOULD notify clients when a user they're interacting with
|
"Servers SHOULD notify clients when a user they're interacting with
|
||||||
is away when relevant"
|
is away when relevant"
|
||||||
-- https://modern.ircdocs.horse/#away-message
|
-- https://github.com/ircdocs/modern-irc/pull/100
|
||||||
|
|
||||||
"<client> <nick> :<message>"
|
"<client> <nick> :<message>"
|
||||||
-- https://modern.ircdocs.horse/#rplaway-301
|
-- https://modern.ircdocs.horse/#rplaway-301
|
||||||
@ -124,7 +113,7 @@ class AwayTestCase(cases.BaseServerTestCase):
|
|||||||
"""
|
"""
|
||||||
"Servers SHOULD notify clients when a user they're interacting with
|
"Servers SHOULD notify clients when a user they're interacting with
|
||||||
is away when relevant"
|
is away when relevant"
|
||||||
-- https://modern.ircdocs.horse/#away-message
|
-- https://github.com/ircdocs/modern-irc/pull/100
|
||||||
|
|
||||||
"<client> <nick> :<message>"
|
"<client> <nick> :<message>"
|
||||||
-- https://modern.ircdocs.horse/#rplaway-301
|
-- https://modern.ircdocs.horse/#rplaway-301
|
||||||
@ -145,33 +134,3 @@ class AwayTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
self.getMessage(2), command=RPL_USERHOST, params=["qux", StrRe(r"bar=-.*")]
|
self.getMessage(2), command=RPL_USERHOST, params=["qux", StrRe(r"bar=-.*")]
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testAwayEmptyMessage(self):
|
|
||||||
"""
|
|
||||||
"If [AWAY] is sent with a nonempty parameter (the 'away message')
|
|
||||||
then the user is set to be away. If this command is sent with no
|
|
||||||
parameters, or with the empty string as the parameter, the user is no
|
|
||||||
longer away."
|
|
||||||
-- https://modern.ircdocs.horse/#away-message
|
|
||||||
"""
|
|
||||||
self.connectClient("bar", name="bar")
|
|
||||||
self.connectClient("qux", name="qux")
|
|
||||||
|
|
||||||
self.sendLine("bar", "AWAY :I'm not here right now")
|
|
||||||
replies = self.getMessages("bar")
|
|
||||||
self.assertIn(RPL_NOWAWAY, [msg.command for msg in replies])
|
|
||||||
self.sendLine("qux", "WHOIS bar")
|
|
||||||
replies = self.getMessages("qux")
|
|
||||||
self.assertIn(RPL_WHOISUSER, [msg.command for msg in replies])
|
|
||||||
self.assertIn(RPL_AWAY, [msg.command for msg in replies])
|
|
||||||
|
|
||||||
# empty final parameter to AWAY is treated the same as no parameter,
|
|
||||||
# i.e., the client is considered to be no longer away
|
|
||||||
self.sendLine("bar", "AWAY :")
|
|
||||||
replies = self.getMessages("bar")
|
|
||||||
self.assertIn(RPL_UNAWAY, [msg.command for msg in replies])
|
|
||||||
self.sendLine("qux", "WHOIS bar")
|
|
||||||
replies = self.getMessages("qux")
|
|
||||||
self.assertIn(RPL_WHOISUSER, [msg.command for msg in replies])
|
|
||||||
self.assertNotIn(RPL_AWAY, [msg.command for msg in replies])
|
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
`IRCv3 away-notify <https://ircv3.net/specs/extensions/away-notify>`_
|
<https://ircv3.net/specs/extensions/away-notify-3.1>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
|
|
||||||
|
|
||||||
class AwayNotifyTestCase(cases.BaseServerTestCase):
|
class AwayNotifyTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||||
@cases.mark_capabilities("away-notify")
|
@cases.mark_capabilities("away-notify")
|
||||||
def testAwayNotify(self):
|
def testAwayNotify(self):
|
||||||
"""Basic away-notify test."""
|
"""Basic away-notify test."""
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
`IRCv3 bot mode <https://ircv3.net/specs/extensions/bot-mode>`_
|
Draft bot mode specification, as defined in
|
||||||
|
<https://ircv3.net/specs/extensions/bot-mode>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases, runner
|
from irctest import cases, runner
|
||||||
@ -67,10 +68,6 @@ class BotModeTestCase(cases.BaseServerTestCase):
|
|||||||
message, command=RPL_WHOISBOT, params=["usernick", "botnick", ANYSTR]
|
message, command=RPL_WHOISBOT, params=["usernick", "botnick", ANYSTR]
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["InspIRCd"],
|
|
||||||
"Uses only vendor tags for now: https://github.com/inspircd/inspircd/pull/1910",
|
|
||||||
)
|
|
||||||
def testBotPrivateMessage(self):
|
def testBotPrivateMessage(self):
|
||||||
self._initBot()
|
self._initBot()
|
||||||
|
|
||||||
@ -85,13 +82,9 @@ class BotModeTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessage("user"),
|
self.getMessage("user"),
|
||||||
command="PRIVMSG",
|
command="PRIVMSG",
|
||||||
params=["usernick", "beep boop"],
|
params=["usernick", "beep boop"],
|
||||||
tags={StrRe("(draft/)?bot"): None, **ANYDICT},
|
tags={"draft/bot": None, **ANYDICT},
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["InspIRCd"],
|
|
||||||
"Uses only vendor tags for now: https://github.com/inspircd/inspircd/pull/1910",
|
|
||||||
)
|
|
||||||
def testBotChannelMessage(self):
|
def testBotChannelMessage(self):
|
||||||
self._initBot()
|
self._initBot()
|
||||||
|
|
||||||
@ -111,7 +104,7 @@ class BotModeTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessage("user"),
|
self.getMessage("user"),
|
||||||
command="PRIVMSG",
|
command="PRIVMSG",
|
||||||
params=["#chan", "beep boop"],
|
params=["#chan", "beep boop"],
|
||||||
tags={StrRe("(draft/)?bot"): None, **ANYDICT},
|
tags={"draft/bot": None, **ANYDICT},
|
||||||
)
|
)
|
||||||
|
|
||||||
def testBotWhox(self):
|
def testBotWhox(self):
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
"""
|
|
||||||
`Ergo <https://ergo.chat/>`_-specific tests of
|
|
||||||
`multiclient features
|
|
||||||
<https://github.com/ergochat/ergo/blob/master/docs/MANUAL.md#multiclient-bouncer>`_
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.irc_utils.sasl import sasl_plain_blob
|
from irctest.irc_utils.sasl import sasl_plain_blob
|
||||||
from irctest.numerics import ERR_NICKNAMEINUSE, RPL_WELCOME
|
from irctest.numerics import ERR_NICKNAMEINUSE, RPL_WELCOME
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
"""
|
"""Sends packets with various length to check the server reassembles them
|
||||||
Sends packets with various length to check the server reassembles them
|
correctly. Also checks truncation"""
|
||||||
correctly. Also checks truncation.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
@ -32,16 +30,6 @@ def _sendBytePerByte(self, line):
|
|||||||
|
|
||||||
|
|
||||||
class BufferingTestCase(cases.BaseServerTestCase):
|
class BufferingTestCase(cases.BaseServerTestCase):
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["Bahamut"],
|
|
||||||
"cannot pass because of issues with UTF-8 handling: "
|
|
||||||
"https://github.com/DALnet/bahamut/issues/196",
|
|
||||||
)
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["ircu2", "Nefarious", "snircd"],
|
|
||||||
"ircu2 discards the whole buffer on long lines "
|
|
||||||
"(TODO: refine how we exclude these tests)",
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"sender_function,colon",
|
"sender_function,colon",
|
||||||
[
|
[
|
||||||
|
@ -1,35 +1,9 @@
|
|||||||
"""
|
|
||||||
`IRCv3 Capability negotiation
|
|
||||||
<https://ircv3.net/specs/extensions/capability-negotiation>`_
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.patma import ANYSTR, StrRe
|
from irctest.patma import ANYSTR
|
||||||
from irctest.runner import CapabilityNotSupported, ImplementationChoice
|
from irctest.runner import CapabilityNotSupported, ImplementationChoice
|
||||||
|
|
||||||
|
|
||||||
class CapTestCase(cases.BaseServerTestCase):
|
class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
def testInvalidCapSubcommand(self):
|
|
||||||
"""“If no capabilities are active, an empty parameter must be sent.”
|
|
||||||
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-list-subcommand>
|
|
||||||
""" # noqa
|
|
||||||
self.addClient()
|
|
||||||
self.sendLine(1, "CAP NOTACOMMAND")
|
|
||||||
self.sendLine(1, "PING test123")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertTrue(
|
|
||||||
self.messageDiffers(m, command="PONG", params=[ANYSTR, "test123"]),
|
|
||||||
"Sending “CAP NOTACOMMAND” as first message got no reply",
|
|
||||||
)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="410",
|
|
||||||
params=["*", "NOTACOMMAND", ANYSTR],
|
|
||||||
fail_msg="Sending “CAP NOTACOMMAND” as first message got a reply "
|
|
||||||
"that is not ERR_INVALIDCAPCMD: {msg}",
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
def testNoReq(self):
|
def testNoReq(self):
|
||||||
"""Test the server handles gracefully clients which do not send
|
"""Test the server handles gracefully clients which do not send
|
||||||
@ -44,206 +18,12 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
self.getCapLs(1)
|
self.getCapLs(1)
|
||||||
self.sendLine(1, "USER foo foo foo :foo")
|
self.sendLine(1, "USER foo foo foo :foo")
|
||||||
self.sendLine(1, "NICK foo")
|
self.sendLine(1, "NICK foo")
|
||||||
|
|
||||||
# Make sure the server didn't send anything yet
|
|
||||||
self.sendLine(1, "CAP LS 302")
|
|
||||||
self.getCapLs(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP END")
|
self.sendLine(1, "CAP END")
|
||||||
m = self.getRegistrationMessage(1)
|
m = self.getRegistrationMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
def testReqOne(self):
|
|
||||||
"""Tests requesting a single capability"""
|
|
||||||
self.addClient(1)
|
|
||||||
self.sendLine(1, "CAP LS")
|
|
||||||
self.getCapLs(1)
|
|
||||||
self.sendLine(1, "USER foo foo foo :foo")
|
|
||||||
self.sendLine(1, "NICK foo")
|
|
||||||
self.sendLine(1, "CAP REQ :multi-prefix")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
|
|
||||||
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP LIST")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=[ANYSTR, "LIST", StrRe("multi-prefix ?")],
|
|
||||||
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP END")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["ngIRCd"],
|
|
||||||
"ngIRCd does not support userhost-in-names",
|
|
||||||
)
|
|
||||||
def testReqTwo(self):
|
|
||||||
"""Tests requesting two capabilities at once"""
|
|
||||||
self.addClient(1)
|
|
||||||
self.sendLine(1, "CAP LS")
|
|
||||||
self.getCapLs(1)
|
|
||||||
self.sendLine(1, "USER foo foo foo :foo")
|
|
||||||
self.sendLine(1, "NICK foo")
|
|
||||||
self.sendLine(1, "CAP REQ :multi-prefix userhost-in-names")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=[ANYSTR, "ACK", StrRe("multi-prefix userhost-in-names ?")],
|
|
||||||
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP LIST")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=[
|
|
||||||
ANYSTR,
|
|
||||||
"LIST",
|
|
||||||
StrRe(
|
|
||||||
"(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP END")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["ngIRCd"],
|
|
||||||
"ngIRCd does not support userhost-in-names",
|
|
||||||
)
|
|
||||||
def testReqOneThenOne(self):
|
|
||||||
"""Tests requesting two capabilities in different messages"""
|
|
||||||
self.addClient(1)
|
|
||||||
self.sendLine(1, "CAP LS")
|
|
||||||
self.getCapLs(1)
|
|
||||||
self.sendLine(1, "USER foo foo foo :foo")
|
|
||||||
self.sendLine(1, "NICK foo")
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP REQ :multi-prefix")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
|
|
||||||
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP REQ :userhost-in-names")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=[ANYSTR, "ACK", StrRe("userhost-in-names ?")],
|
|
||||||
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP LIST")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=[
|
|
||||||
ANYSTR,
|
|
||||||
"LIST",
|
|
||||||
StrRe(
|
|
||||||
"(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP END")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["ngIRCd"],
|
|
||||||
"ngIRCd does not support userhost-in-names",
|
|
||||||
)
|
|
||||||
def testReqPostRegistration(self):
|
|
||||||
"""Tests requesting more capabilities after CAP END"""
|
|
||||||
self.addClient(1)
|
|
||||||
self.sendLine(1, "CAP LS")
|
|
||||||
self.getCapLs(1)
|
|
||||||
self.sendLine(1, "USER foo foo foo :foo")
|
|
||||||
self.sendLine(1, "NICK foo")
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP REQ :multi-prefix")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
|
|
||||||
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP LIST")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=[ANYSTR, "LIST", StrRe("multi-prefix ?")],
|
|
||||||
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP END")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}."
|
|
||||||
)
|
|
||||||
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP REQ :userhost-in-names")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=[ANYSTR, "ACK", StrRe("userhost-in-names ?")],
|
|
||||||
fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "CAP LIST")
|
|
||||||
m = self.getMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=[
|
|
||||||
ANYSTR,
|
|
||||||
"LIST",
|
|
||||||
StrRe(
|
|
||||||
"(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?"
|
|
||||||
),
|
|
||||||
],
|
|
||||||
fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
def testReqUnavailable(self):
|
def testReqUnavailable(self):
|
||||||
"""Test the server handles gracefully clients which request
|
"""Test the server handles gracefully clients which request
|
||||||
@ -260,7 +40,7 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
command="CAP",
|
command="CAP",
|
||||||
params=[ANYSTR, "NAK", StrRe("foo ?")],
|
params=[ANYSTR, "NAK", "foo"],
|
||||||
fail_msg="Expected CAP NAK after requesting non-existing "
|
fail_msg="Expected CAP NAK after requesting non-existing "
|
||||||
"capability, got {msg}.",
|
"capability, got {msg}.",
|
||||||
)
|
)
|
||||||
@ -334,7 +114,7 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
command="CAP",
|
command="CAP",
|
||||||
params=[ANYSTR, "ACK", StrRe("multi-prefix ?")],
|
params=[ANYSTR, "ACK", "multi-prefix"],
|
||||||
fail_msg="Expected “CAP ACK :multi-prefix” after "
|
fail_msg="Expected “CAP ACK :multi-prefix” after "
|
||||||
"sending “CAP REQ :multi-prefix”, but got {msg}.",
|
"sending “CAP REQ :multi-prefix”, but got {msg}.",
|
||||||
)
|
)
|
||||||
@ -347,13 +127,8 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
self.addClient(1)
|
self.addClient(1)
|
||||||
self.connectClient("sender")
|
self.connectClient("sender")
|
||||||
self.sendLine(1, "CAP LS 302")
|
self.sendLine(1, "CAP LS 302")
|
||||||
caps = set()
|
m = self.getRegistrationMessage(1)
|
||||||
while True:
|
if not ({cap1, cap2} <= set(m.params[2].split())):
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
caps.update(m.params[-1].split())
|
|
||||||
if m.params[2] != "*":
|
|
||||||
break
|
|
||||||
if not ({cap1, cap2} <= caps):
|
|
||||||
raise CapabilityNotSupported(f"{cap1} or {cap2}")
|
raise CapabilityNotSupported(f"{cap1} or {cap2}")
|
||||||
self.sendLine(1, f"CAP REQ :{cap1} {cap2}")
|
self.sendLine(1, f"CAP REQ :{cap1} {cap2}")
|
||||||
self.sendLine(1, "nick bar")
|
self.sendLine(1, "nick bar")
|
||||||
@ -379,19 +154,17 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertIn("time", m.tags, m)
|
self.assertIn("time", m.tags, m)
|
||||||
|
|
||||||
# remove the multi-prefix cap
|
# remove the server-time cap
|
||||||
self.sendLine(1, f"CAP REQ :-{cap2}")
|
self.sendLine(1, f"CAP REQ :-{cap2}")
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
# Must be either ACK or NAK
|
# Must be either ACK or NAK
|
||||||
if self.messageDiffers(
|
if self.messageDiffers(m, command="CAP", params=[ANYSTR, "ACK", f"-{cap2}"]):
|
||||||
m, command="CAP", params=[ANYSTR, "ACK", StrRe(f"-{cap2} ?")]
|
|
||||||
):
|
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m, command="CAP", params=[ANYSTR, "NAK", StrRe(f"-{cap2} ?")]
|
m, command="CAP", params=[ANYSTR, "NAK", f"-{cap2}"]
|
||||||
)
|
)
|
||||||
raise ImplementationChoice(f"Does not support CAP REQ -{cap2}")
|
raise ImplementationChoice(f"Does not support CAP REQ -{cap2}")
|
||||||
|
|
||||||
# multi-prefix should be disabled
|
# server-time should be disabled
|
||||||
self.sendLine(1, "CAP LIST")
|
self.sendLine(1, "CAP LIST")
|
||||||
messages = self.getMessages(1)
|
messages = self.getMessages(1)
|
||||||
cap_list = [m for m in messages if m.command == "CAP"][0]
|
cap_list = [m for m in messages if m.command == "CAP"][0]
|
||||||
@ -399,88 +172,3 @@ class CapTestCase(cases.BaseServerTestCase):
|
|||||||
enabled_caps.discard("cap-notify") # implicitly added by some impls
|
enabled_caps.discard("cap-notify") # implicitly added by some impls
|
||||||
self.assertEqual(enabled_caps, {cap1})
|
self.assertEqual(enabled_caps, {cap1})
|
||||||
self.assertNotIn("time", cap_list.tags)
|
self.assertNotIn("time", cap_list.tags)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
def testIrc301CapLs(self):
|
|
||||||
"""
|
|
||||||
Current version:
|
|
||||||
|
|
||||||
"The LS subcommand is used to list the capabilities supported by the server.
|
|
||||||
The client should send an LS subcommand with no other arguments to solicit
|
|
||||||
a list of all capabilities."
|
|
||||||
|
|
||||||
"If a client has not indicated support for CAP LS 302 features,
|
|
||||||
the server MUST NOT send these new features to the client."
|
|
||||||
-- <https://ircv3.net/specs/core/capability-negotiation.html>
|
|
||||||
|
|
||||||
Before the v3.1 / v3.2 merge:
|
|
||||||
|
|
||||||
IRCv3.1: “The LS subcommand is used to list the capabilities
|
|
||||||
supported by the server. The client should send an LS subcommand with
|
|
||||||
no other arguments to solicit a list of all capabilities.”
|
|
||||||
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-ls-subcommand>
|
|
||||||
|
|
||||||
IRCv3.2: “Servers MUST NOT send messages described by this document if
|
|
||||||
the client only supports version 3.1.”
|
|
||||||
-- <http://ircv3.net/specs/core/capability-negotiation-3.2.html#version-in-cap-ls>
|
|
||||||
""" # noqa
|
|
||||||
self.addClient()
|
|
||||||
self.sendLine(1, "CAP LS")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertNotEqual(
|
|
||||||
m.params[2],
|
|
||||||
"*",
|
|
||||||
m,
|
|
||||||
fail_msg="Server replied with multi-line CAP LS to a "
|
|
||||||
"“CAP LS” (ie. IRCv3.1) request: {msg}",
|
|
||||||
)
|
|
||||||
self.assertFalse(
|
|
||||||
any("=" in cap for cap in m.params[2].split()),
|
|
||||||
"Server replied with a name-value capability in "
|
|
||||||
"CAP LS reply as a response to “CAP LS” (ie. IRCv3.1) "
|
|
||||||
"request: {}".format(m),
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
def testEmptyCapList(self):
|
|
||||||
"""“If no capabilities are active, an empty parameter must be sent.”
|
|
||||||
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-list-subcommand>
|
|
||||||
""" # noqa
|
|
||||||
self.addClient()
|
|
||||||
self.sendLine(1, "CAP LIST")
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="CAP",
|
|
||||||
params=["*", "LIST", ""],
|
|
||||||
fail_msg="Sending “CAP LIST” as first message got a reply "
|
|
||||||
"that is not “CAP * LIST :”: {msg}",
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
def testNoMultiline301Response(self):
|
|
||||||
"""
|
|
||||||
Current version: "If the client supports CAP version 302, the server MAY send
|
|
||||||
multiple lines in response to CAP LS and CAP LIST." This should be read as
|
|
||||||
disallowing multiline responses to pre-302 clients.
|
|
||||||
-- <https://ircv3.net/specs/extensions/capability-negotiation#multiline-replies-to-cap-ls-and-cap-list>
|
|
||||||
""" # noqa
|
|
||||||
self.check301ResponsePreRegistration("bar", "CAP LS")
|
|
||||||
self.check301ResponsePreRegistration("qux", "CAP LS 301")
|
|
||||||
self.check301ResponsePostRegistration("baz", "CAP LS")
|
|
||||||
self.check301ResponsePostRegistration("bat", "CAP LS 301")
|
|
||||||
|
|
||||||
def check301ResponsePreRegistration(self, nick, cap_ls):
|
|
||||||
self.addClient(nick)
|
|
||||||
self.sendLine(nick, cap_ls)
|
|
||||||
self.sendLine(nick, "NICK " + nick)
|
|
||||||
self.sendLine(nick, "USER u s e r")
|
|
||||||
self.sendLine(nick, "CAP END")
|
|
||||||
responses = [msg for msg in self.skipToWelcome(nick) if msg.command == "CAP"]
|
|
||||||
self.assertLessEqual(len(responses), 1, responses)
|
|
||||||
|
|
||||||
def check301ResponsePostRegistration(self, nick, cap_ls):
|
|
||||||
self.connectClient(nick, name=nick)
|
|
||||||
self.sendLine(nick, cap_ls)
|
|
||||||
responses = [msg for msg in self.getMessages(nick) if msg.command == "CAP"]
|
|
||||||
self.assertLessEqual(len(responses), 1, responses)
|
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
"""
|
|
||||||
Channel casemapping
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from irctest import cases, client_mock, runner
|
from irctest import cases, client_mock, runner
|
||||||
@ -22,7 +18,7 @@ class ChannelCaseSensitivityTestCase(cases.BaseServerTestCase):
|
|||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
if self.server_support["CASEMAPPING"] != casemapping:
|
if self.server_support["CASEMAPPING"] != casemapping:
|
||||||
raise runner.ImplementationChoice(
|
raise runner.NotImplementedByController(
|
||||||
"Casemapping {} not implemented".format(casemapping)
|
"Casemapping {} not implemented".format(casemapping)
|
||||||
)
|
)
|
||||||
self.joinClient(1, name1)
|
self.joinClient(1, name1)
|
||||||
@ -47,7 +43,7 @@ class ChannelCaseSensitivityTestCase(cases.BaseServerTestCase):
|
|||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
if self.server_support["CASEMAPPING"] != casemapping:
|
if self.server_support["CASEMAPPING"] != casemapping:
|
||||||
raise runner.ImplementationChoice(
|
raise runner.NotImplementedByController(
|
||||||
"Casemapping {} not implemented".format(casemapping)
|
"Casemapping {} not implemented".format(casemapping)
|
||||||
)
|
)
|
||||||
self.joinClient(1, name1)
|
self.joinClient(1, name1)
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
"""
|
|
||||||
`Ergo <https://ergo.chat/>`_-specific tests of channel forwarding
|
|
||||||
|
|
||||||
TODO: Should be extended to other servers, once a specification is written.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import ERR_CHANOPRIVSNEEDED, ERR_INVALIDMODEPARAM, ERR_LINKCHANNEL
|
from irctest.numerics import ERR_CHANOPRIVSNEEDED, ERR_INVALIDMODEPARAM, ERR_LINKCHANNEL
|
||||||
|
|
||||||
|
@ -1,22 +1,24 @@
|
|||||||
"""
|
|
||||||
`Draft IRCv3 channel-rename <https://ircv3.net/specs/extensions/channel-rename>`_
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import ERR_CHANOPRIVSNEEDED
|
from irctest.numerics import ERR_CHANOPRIVSNEEDED
|
||||||
|
|
||||||
|
MODERN_CAPS = [
|
||||||
|
"server-time",
|
||||||
|
"message-tags",
|
||||||
|
"batch",
|
||||||
|
"labeled-response",
|
||||||
|
"echo-message",
|
||||||
|
"account-tag",
|
||||||
|
]
|
||||||
RENAME_CAP = "draft/channel-rename"
|
RENAME_CAP = "draft/channel-rename"
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
class ChannelRenameTestCase(cases.BaseServerTestCase):
|
class ChannelRenameTestCase(cases.BaseServerTestCase):
|
||||||
"""Basic tests for channel-rename."""
|
"""Basic tests for channel-rename."""
|
||||||
|
|
||||||
|
@cases.mark_specifications("Ergo")
|
||||||
def testChannelRename(self):
|
def testChannelRename(self):
|
||||||
self.connectClient(
|
self.connectClient("bar", name="bar", capabilities=MODERN_CAPS + [RENAME_CAP])
|
||||||
"bar", name="bar", capabilities=[RENAME_CAP], skip_if_cap_nak=True
|
self.connectClient("baz", name="baz", capabilities=MODERN_CAPS)
|
||||||
)
|
|
||||||
self.connectClient("baz", name="baz")
|
|
||||||
self.joinChannel("bar", "#bar")
|
self.joinChannel("bar", "#bar")
|
||||||
self.joinChannel("baz", "#bar")
|
self.joinChannel("baz", "#bar")
|
||||||
self.getMessages("bar")
|
self.getMessages("bar")
|
||||||
|
@ -1,16 +1,11 @@
|
|||||||
"""
|
|
||||||
`IRCv3 draft chathistory <https://ircv3.net/specs/extensions/chathistory>`_
|
|
||||||
"""
|
|
||||||
|
|
||||||
import functools
|
|
||||||
import secrets
|
import secrets
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from irctest import cases, runner
|
from irctest import cases
|
||||||
from irctest.irc_utils.junkdrawer import random_name
|
from irctest.irc_utils.junkdrawer import random_name
|
||||||
from irctest.patma import ANYSTR, StrRe
|
from irctest.patma import ANYSTR
|
||||||
|
|
||||||
CHATHISTORY_CAP = "draft/chathistory"
|
CHATHISTORY_CAP = "draft/chathistory"
|
||||||
EVENT_PLAYBACK_CAP = "draft/event-playback"
|
EVENT_PLAYBACK_CAP = "draft/event-playback"
|
||||||
@ -21,44 +16,35 @@ SUBCOMMANDS = ["LATEST", "BEFORE", "AFTER", "BETWEEN", "AROUND"]
|
|||||||
MYSQL_PASSWORD = ""
|
MYSQL_PASSWORD = ""
|
||||||
|
|
||||||
|
|
||||||
def skip_ngircd(f):
|
def validate_chathistory_batch(msgs):
|
||||||
@functools.wraps(f)
|
batch_tag = None
|
||||||
def newf(self, *args, **kwargs):
|
closed_batch_tag = None
|
||||||
if self.controller.software_name == "ngIRCd":
|
result = []
|
||||||
raise runner.OptionalExtensionNotSupported("nicks longer 9 characters")
|
for msg in msgs:
|
||||||
return f(self, *args, **kwargs)
|
if msg.command == "BATCH":
|
||||||
|
batch_param = msg.params[0]
|
||||||
return newf
|
if batch_tag is None and batch_param[0] == "+":
|
||||||
|
batch_tag = batch_param[1:]
|
||||||
|
elif batch_param[0] == "-":
|
||||||
|
closed_batch_tag = batch_param[1:]
|
||||||
|
elif (
|
||||||
|
msg.command == "PRIVMSG"
|
||||||
|
and batch_tag is not None
|
||||||
|
and msg.tags.get("batch") == batch_tag
|
||||||
|
):
|
||||||
|
if not msg.prefix.startswith("HistServ!"): # FIXME: ergo-specific
|
||||||
|
result.append(msg.to_history_message())
|
||||||
|
assert batch_tag == closed_batch_tag
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.mark_services
|
@cases.mark_services
|
||||||
class ChathistoryTestCase(cases.BaseServerTestCase):
|
class ChathistoryTestCase(cases.BaseServerTestCase):
|
||||||
def validate_chathistory_batch(self, msgs, target):
|
|
||||||
(start, *inner_msgs, end) = msgs
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
start, command="BATCH", params=[StrRe(r"\+.*"), "chathistory", target]
|
|
||||||
)
|
|
||||||
batch_tag = start.params[0][1:]
|
|
||||||
self.assertMessageMatch(end, command="BATCH", params=["-" + batch_tag])
|
|
||||||
|
|
||||||
result = []
|
|
||||||
for msg in inner_msgs:
|
|
||||||
if (
|
|
||||||
msg.command == "PRIVMSG"
|
|
||||||
and batch_tag is not None
|
|
||||||
and msg.tags.get("batch") == batch_tag
|
|
||||||
):
|
|
||||||
if not msg.prefix.startswith("HistServ!"): # FIXME: ergo-specific
|
|
||||||
result.append(msg.to_history_message())
|
|
||||||
return result
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def config() -> cases.TestCaseControllerConfig:
|
def config() -> cases.TestCaseControllerConfig:
|
||||||
return cases.TestCaseControllerConfig(chathistory=True)
|
return cases.TestCaseControllerConfig(chathistory=True)
|
||||||
|
|
||||||
@skip_ngircd
|
|
||||||
def testInvalidTargets(self):
|
def testInvalidTargets(self):
|
||||||
bar, pw = random_name("bar"), random_name("pw")
|
bar, pw = random_name("bar"), random_name("pw")
|
||||||
self.controller.registerUser(self, bar, pw)
|
self.controller.registerUser(self, bar, pw)
|
||||||
@ -104,7 +90,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.private_chathistory
|
@pytest.mark.private_chathistory
|
||||||
@skip_ngircd
|
|
||||||
def testMessagesToSelf(self):
|
def testMessagesToSelf(self):
|
||||||
bar, pw = random_name("bar"), random_name("pw")
|
bar, pw = random_name("bar"), random_name("pw")
|
||||||
self.controller.registerUser(self, bar, pw)
|
self.controller.registerUser(self, bar, pw)
|
||||||
@ -177,19 +162,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertEqual(len(set(msg.time for msg in echo_messages)), num_messages)
|
self.assertEqual(len(set(msg.time for msg in echo_messages)), num_messages)
|
||||||
|
|
||||||
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
|
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
|
||||||
@skip_ngircd
|
|
||||||
def testChathistory(self, subcommand):
|
def testChathistory(self, subcommand):
|
||||||
if subcommand == "BETWEEN" and self.controller.software_name == "UnrealIRCd":
|
|
||||||
pytest.xfail(
|
|
||||||
"CHATHISTORY BETWEEN does not apply bounds correct "
|
|
||||||
"https://bugs.unrealircd.org/view.php?id=5952"
|
|
||||||
)
|
|
||||||
if subcommand == "AROUND" and self.controller.software_name == "UnrealIRCd":
|
|
||||||
pytest.xfail(
|
|
||||||
"CHATHISTORY AROUND excludes 'central' messages "
|
|
||||||
"https://bugs.unrealircd.org/view.php?id=5953"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
"bar",
|
"bar",
|
||||||
capabilities=[
|
capabilities=[
|
||||||
@ -221,7 +194,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
self.validate_chathistory(subcommand, echo_messages, 1, chname)
|
self.validate_chathistory(subcommand, echo_messages, 1, chname)
|
||||||
|
|
||||||
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
|
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
|
||||||
@skip_ngircd
|
|
||||||
def testChathistoryEventPlayback(self, subcommand):
|
def testChathistoryEventPlayback(self, subcommand):
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
"bar",
|
"bar",
|
||||||
@ -255,7 +227,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
|
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
|
||||||
@pytest.mark.private_chathistory
|
@pytest.mark.private_chathistory
|
||||||
@skip_ngircd
|
|
||||||
def testChathistoryDMs(self, subcommand):
|
def testChathistoryDMs(self, subcommand):
|
||||||
c1 = "foo" + secrets.token_hex(12)
|
c1 = "foo" + secrets.token_hex(12)
|
||||||
c2 = "bar" + secrets.token_hex(12)
|
c2 = "bar" + secrets.token_hex(12)
|
||||||
@ -306,9 +277,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
time.sleep(0.002)
|
time.sleep(0.002)
|
||||||
|
|
||||||
self.getMessages(1)
|
|
||||||
self.getMessages(2)
|
|
||||||
|
|
||||||
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
|
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
|
||||||
self.validate_chathistory(subcommand, echo_messages, 1, c2)
|
self.validate_chathistory(subcommand, echo_messages, 1, c2)
|
||||||
self.validate_chathistory(subcommand, echo_messages, 2, c1)
|
self.validate_chathistory(subcommand, echo_messages, 2, c1)
|
||||||
@ -402,15 +370,15 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
def _validate_chathistory_LATEST(self, echo_messages, user, chname):
|
def _validate_chathistory_LATEST(self, echo_messages, user, chname):
|
||||||
INCLUSIVE_LIMIT = len(echo_messages) * 2
|
INCLUSIVE_LIMIT = len(echo_messages) * 2
|
||||||
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, INCLUSIVE_LIMIT))
|
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, INCLUSIVE_LIMIT))
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages, result)
|
self.assertEqual(echo_messages, result)
|
||||||
|
|
||||||
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 5))
|
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 5))
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[-5:], result)
|
self.assertEqual(echo_messages[-5:], result)
|
||||||
|
|
||||||
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 1))
|
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 1))
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[-1:], result)
|
self.assertEqual(echo_messages[-1:], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
@ -418,7 +386,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
"CHATHISTORY LATEST %s msgid=%s %d"
|
"CHATHISTORY LATEST %s msgid=%s %d"
|
||||||
% (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT),
|
% (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[5:], result)
|
self.assertEqual(echo_messages[5:], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
@ -426,7 +394,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
"CHATHISTORY LATEST %s timestamp=%s %d"
|
"CHATHISTORY LATEST %s timestamp=%s %d"
|
||||||
% (chname, echo_messages[4].time, INCLUSIVE_LIMIT),
|
% (chname, echo_messages[4].time, INCLUSIVE_LIMIT),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[5:], result)
|
self.assertEqual(echo_messages[5:], result)
|
||||||
|
|
||||||
def _validate_chathistory_BEFORE(self, echo_messages, user, chname):
|
def _validate_chathistory_BEFORE(self, echo_messages, user, chname):
|
||||||
@ -436,7 +404,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
"CHATHISTORY BEFORE %s msgid=%s %d"
|
"CHATHISTORY BEFORE %s msgid=%s %d"
|
||||||
% (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT),
|
% (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[:6], result)
|
self.assertEqual(echo_messages[:6], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
@ -444,7 +412,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
"CHATHISTORY BEFORE %s timestamp=%s %d"
|
"CHATHISTORY BEFORE %s timestamp=%s %d"
|
||||||
% (chname, echo_messages[6].time, INCLUSIVE_LIMIT),
|
% (chname, echo_messages[6].time, INCLUSIVE_LIMIT),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[:6], result)
|
self.assertEqual(echo_messages[:6], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
@ -452,7 +420,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
"CHATHISTORY BEFORE %s timestamp=%s %d"
|
"CHATHISTORY BEFORE %s timestamp=%s %d"
|
||||||
% (chname, echo_messages[6].time, 2),
|
% (chname, echo_messages[6].time, 2),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[4:6], result)
|
self.assertEqual(echo_messages[4:6], result)
|
||||||
|
|
||||||
def _validate_chathistory_AFTER(self, echo_messages, user, chname):
|
def _validate_chathistory_AFTER(self, echo_messages, user, chname):
|
||||||
@ -462,7 +430,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
"CHATHISTORY AFTER %s msgid=%s %d"
|
"CHATHISTORY AFTER %s msgid=%s %d"
|
||||||
% (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT),
|
% (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[4:], result)
|
self.assertEqual(echo_messages[4:], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
@ -470,14 +438,14 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
"CHATHISTORY AFTER %s timestamp=%s %d"
|
"CHATHISTORY AFTER %s timestamp=%s %d"
|
||||||
% (chname, echo_messages[3].time, INCLUSIVE_LIMIT),
|
% (chname, echo_messages[3].time, INCLUSIVE_LIMIT),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[4:], result)
|
self.assertEqual(echo_messages[4:], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, 3),
|
"CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, 3),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[4:7], result)
|
self.assertEqual(echo_messages[4:7], result)
|
||||||
|
|
||||||
def _validate_chathistory_BETWEEN(self, echo_messages, user, chname):
|
def _validate_chathistory_BETWEEN(self, echo_messages, user, chname):
|
||||||
@ -493,7 +461,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
INCLUSIVE_LIMIT,
|
INCLUSIVE_LIMIT,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[1:-1], result)
|
self.assertEqual(echo_messages[1:-1], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
@ -506,7 +474,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
INCLUSIVE_LIMIT,
|
INCLUSIVE_LIMIT,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[1:-1], result)
|
self.assertEqual(echo_messages[1:-1], result)
|
||||||
|
|
||||||
# BETWEEN forwards and backwards with a limit, should get
|
# BETWEEN forwards and backwards with a limit, should get
|
||||||
@ -516,7 +484,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
||||||
% (chname, echo_messages[0].msgid, echo_messages[-1].msgid, 3),
|
% (chname, echo_messages[0].msgid, echo_messages[-1].msgid, 3),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[1:4], result)
|
self.assertEqual(echo_messages[1:4], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
@ -524,7 +492,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
|
||||||
% (chname, echo_messages[-1].msgid, echo_messages[0].msgid, 3),
|
% (chname, echo_messages[-1].msgid, echo_messages[0].msgid, 3),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[-4:-1], result)
|
self.assertEqual(echo_messages[-4:-1], result)
|
||||||
|
|
||||||
# same stuff again but with timestamps
|
# same stuff again but with timestamps
|
||||||
@ -533,28 +501,28 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
||||||
% (chname, echo_messages[0].time, echo_messages[-1].time, INCLUSIVE_LIMIT),
|
% (chname, echo_messages[0].time, echo_messages[-1].time, INCLUSIVE_LIMIT),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[1:-1], result)
|
self.assertEqual(echo_messages[1:-1], result)
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
||||||
% (chname, echo_messages[-1].time, echo_messages[0].time, INCLUSIVE_LIMIT),
|
% (chname, echo_messages[-1].time, echo_messages[0].time, INCLUSIVE_LIMIT),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[1:-1], result)
|
self.assertEqual(echo_messages[1:-1], result)
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
||||||
% (chname, echo_messages[0].time, echo_messages[-1].time, 3),
|
% (chname, echo_messages[0].time, echo_messages[-1].time, 3),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[1:4], result)
|
self.assertEqual(echo_messages[1:4], result)
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
|
||||||
% (chname, echo_messages[-1].time, echo_messages[0].time, 3),
|
% (chname, echo_messages[-1].time, echo_messages[0].time, 3),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[-4:-1], result)
|
self.assertEqual(echo_messages[-4:-1], result)
|
||||||
|
|
||||||
def _validate_chathistory_AROUND(self, echo_messages, user, chname):
|
def _validate_chathistory_AROUND(self, echo_messages, user, chname):
|
||||||
@ -562,14 +530,14 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
user,
|
user,
|
||||||
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 1),
|
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 1),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual([echo_messages[7]], result)
|
self.assertEqual([echo_messages[7]], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
user,
|
user,
|
||||||
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 3),
|
"CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 3),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertEqual(echo_messages[6:9], result)
|
self.assertEqual(echo_messages[6:9], result)
|
||||||
|
|
||||||
self.sendLine(
|
self.sendLine(
|
||||||
@ -577,11 +545,10 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
"CHATHISTORY AROUND %s timestamp=%s %d"
|
"CHATHISTORY AROUND %s timestamp=%s %d"
|
||||||
% (chname, echo_messages[7].time, 3),
|
% (chname, echo_messages[7].time, 3),
|
||||||
)
|
)
|
||||||
result = self.validate_chathistory_batch(self.getMessages(user), chname)
|
result = validate_chathistory_batch(self.getMessages(user))
|
||||||
self.assertIn(echo_messages[7], result)
|
self.assertIn(echo_messages[7], result)
|
||||||
|
|
||||||
@pytest.mark.arbitrary_client_tags
|
@pytest.mark.arbitrary_client_tags
|
||||||
@skip_ngircd
|
|
||||||
def testChathistoryTagmsg(self):
|
def testChathistoryTagmsg(self):
|
||||||
c1 = "foo" + secrets.token_hex(12)
|
c1 = "foo" + secrets.token_hex(12)
|
||||||
c2 = "bar" + secrets.token_hex(12)
|
c2 = "bar" + secrets.token_hex(12)
|
||||||
@ -680,7 +647,6 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
@pytest.mark.arbitrary_client_tags
|
@pytest.mark.arbitrary_client_tags
|
||||||
@pytest.mark.private_chathistory
|
@pytest.mark.private_chathistory
|
||||||
@skip_ngircd
|
|
||||||
def testChathistoryDMClientOnlyTags(self):
|
def testChathistoryDMClientOnlyTags(self):
|
||||||
# regression test for Ergo #1411
|
# regression test for Ergo #1411
|
||||||
c1 = "foo" + secrets.token_hex(12)
|
c1 = "foo" + secrets.token_hex(12)
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
"""
|
|
||||||
`Ergo <https://ergo.chat/>`_-specific tests of auditorium mode
|
|
||||||
|
|
||||||
TODO: Should be extended to other servers, once a specification is written.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import math
|
import math
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
@ -1,18 +1,9 @@
|
|||||||
"""
|
from irctest import cases
|
||||||
Channel ban (`RFC 1459
|
from irctest.numerics import ERR_BANNEDFROMCHAN
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.3.1>`__,
|
|
||||||
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.3>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#ban-channel-mode>`__)
|
|
||||||
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.patma import ANYSTR, StrRe
|
|
||||||
|
|
||||||
|
|
||||||
class BanModeTestCase(cases.BaseServerTestCase):
|
class BanModeTestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
@cases.mark_specifications("RFC1459", "RFC2812")
|
||||||
def testBan(self):
|
def testBan(self):
|
||||||
"""Basic ban operation"""
|
"""Basic ban operation"""
|
||||||
self.connectClient("chanop", name="chanop")
|
self.connectClient("chanop", name="chanop")
|
||||||
@ -32,111 +23,6 @@ class BanModeTestCase(cases.BaseServerTestCase):
|
|||||||
self.sendLine("bar", "JOIN #chan")
|
self.sendLine("bar", "JOIN #chan")
|
||||||
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")
|
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testBanList(self):
|
|
||||||
"""`RPL_BANLIST <https://modern.ircdocs.horse/#rplbanlist-367>`_"""
|
|
||||||
self.connectClient("chanop")
|
|
||||||
self.joinChannel(1, "#chan")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.sendLine(1, "MODE #chan +b bar!*@*")
|
|
||||||
self.assertMessageMatch(self.getMessage(1), command="MODE")
|
|
||||||
|
|
||||||
self.sendLine(1, "MODE #chan +b")
|
|
||||||
|
|
||||||
m = self.getMessage(1)
|
|
||||||
if len(m.params) == 3:
|
|
||||||
# Old format
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command=RPL_BANLIST,
|
|
||||||
params=[
|
|
||||||
"chanop",
|
|
||||||
"#chan",
|
|
||||||
"bar!*@*",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command=RPL_BANLIST,
|
|
||||||
params=[
|
|
||||||
"chanop",
|
|
||||||
"#chan",
|
|
||||||
"bar!*@*",
|
|
||||||
StrRe("chanop(!.*@.*)?"),
|
|
||||||
StrRe("[0-9]+"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=RPL_ENDOFBANLIST,
|
|
||||||
params=[
|
|
||||||
"chanop",
|
|
||||||
"#chan",
|
|
||||||
ANYSTR,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testBanException(self):
|
|
||||||
"""`Exception mode <https://modern.ircdocs.horse/#exception-channel-mode`_,
|
|
||||||
detected using `ISUPPORT EXCEPTS
|
|
||||||
<https://modern.ircdocs.horse/#excepts-parameter>`_ and checked against
|
|
||||||
`ISUPPORT CHANMODES <https://modern.ircdocs.horse/#chanmodes-parameter>`_"""
|
|
||||||
self.connectClient("chanop", name="chanop")
|
|
||||||
|
|
||||||
if "EXCEPTS" in self.server_support:
|
|
||||||
mode = self.server_support["EXCEPTS"] or "e"
|
|
||||||
if "CHANMODES" in self.server_support:
|
|
||||||
self.assertIn(
|
|
||||||
mode,
|
|
||||||
self.server_support["CHANMODES"],
|
|
||||||
fail_msg="ISUPPORT EXCEPTS is present, but '{item}' is missing "
|
|
||||||
"from 'CHANMODES={list}'",
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
mode,
|
|
||||||
self.server_support["CHANMODES"].split(",")[0],
|
|
||||||
fail_msg="ISUPPORT EXCEPTS is present, but '{item}' is not "
|
|
||||||
"in group A",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
mode = "e"
|
|
||||||
if "CHANMODES" in self.server_support:
|
|
||||||
if "e" not in self.server_support["CHANMODES"]:
|
|
||||||
raise runner.OptionalExtensionNotSupported(
|
|
||||||
"Ban exception (or mode letter is not +e)"
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
mode,
|
|
||||||
self.server_support["CHANMODES"].split(",")[0],
|
|
||||||
fail_msg="Mode +e (assumed to be ban exception) is present, "
|
|
||||||
"but 'e' is not in group A",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise runner.OptionalExtensionNotSupported("ISUPPORT CHANMODES")
|
|
||||||
|
|
||||||
self.sendLine("chanop", "JOIN #chan")
|
|
||||||
self.getMessages("chanop")
|
|
||||||
self.sendLine("chanop", "MODE #chan +b ba*!*@*")
|
|
||||||
self.getMessages("chanop")
|
|
||||||
|
|
||||||
# banned client cannot join
|
|
||||||
self.connectClient("Bar", name="bar")
|
|
||||||
self.sendLine("bar", "JOIN #chan")
|
|
||||||
self.assertMessageMatch(self.getMessage("bar"), command=ERR_BANNEDFROMCHAN)
|
|
||||||
|
|
||||||
# chanop sets exception
|
|
||||||
self.sendLine("chanop", "MODE #chan +e *ar!*@*")
|
|
||||||
self.assertMessageMatch(self.getMessage("chanop"), command="MODE")
|
|
||||||
|
|
||||||
# client can now join
|
|
||||||
self.sendLine("bar", "JOIN #chan")
|
|
||||||
self.assertMessageMatch(self.getMessage("bar"), command="JOIN")
|
|
||||||
|
|
||||||
# TODO: Add testBanExceptionList, once the numerics are specified in Modern
|
|
||||||
|
|
||||||
@cases.mark_specifications("Ergo")
|
@cases.mark_specifications("Ergo")
|
||||||
def testCaseInsensitive(self):
|
def testCaseInsensitive(self):
|
||||||
"""Some clients allow unsetting modes if their argument matches
|
"""Some clients allow unsetting modes if their argument matches
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
"""
|
|
||||||
Various Ergo-specific channel modes
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import ERR_CANNOTSENDTOCHAN, ERR_CHANOPRIVSNEEDED
|
from irctest.numerics import ERR_CANNOTSENDTOCHAN, ERR_CHANOPRIVSNEEDED
|
||||||
|
|
||||||
|
@ -1,10 +1,3 @@
|
|||||||
"""
|
|
||||||
Channel key (`RFC 1459
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.3.1>`__,
|
|
||||||
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.3>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#key-channel-mode>`__)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
@ -27,16 +20,10 @@ class KeyTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
self.connectClient("qux")
|
self.connectClient("qux")
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
# JOIN with a missing key MUST receive ERR_BADCHANNELKEY:
|
|
||||||
self.sendLine(2, "JOIN #chan")
|
self.sendLine(2, "JOIN #chan")
|
||||||
reply_cmds = {msg.command for msg in self.getMessages(2)}
|
reply = self.getMessages(2)
|
||||||
self.assertNotIn("JOIN", reply_cmds)
|
self.assertNotIn("JOIN", {msg.command for msg in reply})
|
||||||
self.assertIn(ERR_BADCHANNELKEY, reply_cmds)
|
self.assertIn(ERR_BADCHANNELKEY, {msg.command for msg in reply})
|
||||||
# similarly for JOIN with an incorrect key:
|
|
||||||
self.sendLine(2, "JOIN #chan bees")
|
|
||||||
reply_cmds = {msg.command for msg in self.getMessages(2)}
|
|
||||||
self.assertNotIn("JOIN", reply_cmds)
|
|
||||||
self.assertIn(ERR_BADCHANNELKEY, reply_cmds)
|
|
||||||
|
|
||||||
self.sendLine(2, "JOIN #chan beer")
|
self.sendLine(2, "JOIN #chan beer")
|
||||||
reply = self.getMessages(2)
|
reply = self.getMessages(2)
|
||||||
@ -70,21 +57,6 @@ class KeyTestCase(cases.BaseServerTestCase):
|
|||||||
-- https://modern.ircdocs.horse/#key-channel-mode
|
-- https://modern.ircdocs.horse/#key-channel-mode
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/111
|
-- https://github.com/ircdocs/modern-irc/pull/111
|
||||||
"""
|
"""
|
||||||
if key == "" and self.controller.software_name in (
|
|
||||||
"ircu2",
|
|
||||||
"Nefarious",
|
|
||||||
"snircd",
|
|
||||||
):
|
|
||||||
pytest.xfail(
|
|
||||||
"ircu2 returns ERR_NEEDMOREPARAMS on empty keys: "
|
|
||||||
"https://github.com/UndernetIRC/ircu2/issues/13"
|
|
||||||
)
|
|
||||||
if (key == "" or " " in key) and self.controller.software_name == "ngIRCd":
|
|
||||||
pytest.xfail(
|
|
||||||
"ngIRCd does not validate channel keys: "
|
|
||||||
"https://github.com/ngircd/ngircd/issues/290"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
self.joinChannel(1, "#chan")
|
self.joinChannel(1, "#chan")
|
||||||
self.sendLine(1, f"MODE #chan +k :{key}")
|
self.sendLine(1, f"MODE #chan +k :{key}")
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
"""
|
|
||||||
Channel moderation mode (`RFC 2812
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.3>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#ban-channel-mode>`__)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import ERR_CANNOTSENDTOCHAN
|
from irctest.numerics import ERR_CANNOTSENDTOCHAN
|
||||||
|
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
"""
|
|
||||||
Mute extban, currently no specifications or ways to discover it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases, runner
|
from irctest import cases, runner
|
||||||
from irctest.numerics import ERR_CANNOTSENDTOCHAN, ERR_CHANOPRIVSNEEDED
|
from irctest.numerics import ERR_CANNOTSENDTOCHAN, ERR_CHANOPRIVSNEEDED
|
||||||
from irctest.patma import ANYLIST, StrRe
|
from irctest.patma import ANYLIST, StrRe
|
||||||
@ -20,7 +16,7 @@ class MuteExtbanTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
@cases.mark_specifications("Ergo")
|
@cases.mark_specifications("Ergo")
|
||||||
def testISupport(self):
|
def testISupport(self):
|
||||||
self.connectClient("chk") # Fetches ISUPPORT
|
self.connectClient(1) # Fetches ISUPPORT
|
||||||
isupport = self.server_support
|
isupport = self.server_support
|
||||||
token = isupport["EXTBAN"]
|
token = isupport["EXTBAN"]
|
||||||
prefix, comma, types = token.partition(",")
|
prefix, comma, types = token.partition(",")
|
||||||
@ -198,7 +194,7 @@ class MuteExtbanTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(client)
|
self.getMessages(client)
|
||||||
|
|
||||||
# +e grants an exemption to +b
|
# +e grants an exemption to +b
|
||||||
self.sendLine("chanop", f"MODE #chan +e {prefix}{self.char()}:*!*evan@*")
|
self.sendLine("chanop", f"MODE #chan +e {prefix}{self.char()}:*!~evan@*")
|
||||||
replies = {msg.command for msg in self.getMessages("chanop")}
|
replies = {msg.command for msg in self.getMessages("chanop")}
|
||||||
self.assertIn("MODE", replies)
|
self.assertIn("MODE", replies)
|
||||||
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
|
self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies)
|
||||||
|
@ -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"]
|
|
||||||
)
|
|
@ -1,62 +0,0 @@
|
|||||||
"""
|
|
||||||
Channel secrecy mode (`RFC 1459
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.3.1>`__,
|
|
||||||
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.3>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#secret-channel-mode>`__)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
|
||||||
from irctest.numerics import RPL_LIST
|
|
||||||
|
|
||||||
|
|
||||||
class SecretChannelTestCase(cases.BaseServerTestCase):
|
|
||||||
@cases.mark_specifications("RFC1459", "Modern")
|
|
||||||
def testSecretChannelListCommand(self):
|
|
||||||
"""
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.6>
|
|
||||||
|
|
||||||
"Likewise, secret channels are not listed
|
|
||||||
at all unless the client is a member of the channel in question."
|
|
||||||
|
|
||||||
<https://modern.ircdocs.horse/#secret-channel-mode>
|
|
||||||
"A channel that is set to secret will not show up in responses to
|
|
||||||
the LIST or NAMES command unless the client sending the command is
|
|
||||||
joined to the channel."
|
|
||||||
"""
|
|
||||||
|
|
||||||
def get_listed_channels(replies):
|
|
||||||
channels = set()
|
|
||||||
for reply in replies:
|
|
||||||
# skip pseudo-channels (&SERVER, &NOTICES) listed by ngircd
|
|
||||||
# and ircu:
|
|
||||||
if reply.command == RPL_LIST and reply.params[1].startswith("#"):
|
|
||||||
channels.add(reply.params[1])
|
|
||||||
return channels
|
|
||||||
|
|
||||||
# test that a silent channel is shown in list if the user is in the channel.
|
|
||||||
self.connectClient("first", name="first")
|
|
||||||
self.joinChannel("first", "#gen")
|
|
||||||
self.getMessages("first")
|
|
||||||
self.sendLine("first", "MODE #gen +s")
|
|
||||||
# run command LIST
|
|
||||||
self.sendLine("first", "LIST")
|
|
||||||
replies = self.getMessages("first")
|
|
||||||
self.assertEqual(get_listed_channels(replies), {"#gen"})
|
|
||||||
|
|
||||||
# test that another client would not see the secret
|
|
||||||
# channel.
|
|
||||||
self.connectClient("second", name="second")
|
|
||||||
self.getMessages("second")
|
|
||||||
self.sendLine("second", "LIST")
|
|
||||||
replies = self.getMessages("second")
|
|
||||||
# RPL_LIST 322 should NOT be present this time.
|
|
||||||
self.assertEqual(get_listed_channels(replies), set())
|
|
||||||
|
|
||||||
# Second client will join the secret channel
|
|
||||||
# and call command LIST. The channel SHOULD
|
|
||||||
# appear this time.
|
|
||||||
self.joinChannel("second", "#gen")
|
|
||||||
self.sendLine("second", "LIST")
|
|
||||||
replies = self.getMessages("second")
|
|
||||||
# Should be only one line with command RPL_LIST
|
|
||||||
self.assertEqual(get_listed_channels(replies), {"#gen"})
|
|
@ -1,8 +1,3 @@
|
|||||||
"""
|
|
||||||
`Ergo <https://ergo.chat/>`_-specific tests for nick collisions based on Unicode
|
|
||||||
confusable characters
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import ERR_NICKNAMEINUSE, RPL_WELCOME
|
from irctest.numerics import ERR_NICKNAMEINUSE, RPL_WELCOME
|
||||||
|
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Tests section 4.1 of RFC 1459.
|
Tests section 4.1 of RFC 1459.
|
||||||
<https://tools.ietf.org/html/rfc1459#section-4.1>
|
<https://tools.ietf.org/html/rfc1459#section-4.1>
|
||||||
|
|
||||||
TODO: cross-reference Modern and RFC 2812 too
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.client_mock import ConnectionClosed
|
from irctest.client_mock import ConnectionClosed
|
||||||
from irctest.numerics import ERR_NEEDMOREPARAMS, ERR_PASSWDMISMATCH
|
from irctest.numerics import ERR_NEEDMOREPARAMS
|
||||||
from irctest.patma import ANYSTR, StrRe
|
from irctest.patma import ANYSTR, StrRe
|
||||||
|
|
||||||
|
|
||||||
@ -38,14 +36,8 @@ class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
|||||||
m.command, "001", msg="Got 001 after NICK+USER but missing PASS"
|
m.command, "001", msg="Got 001 after NICK+USER but missing PASS"
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("RFC1459", "RFC2812")
|
||||||
def testWrongPassword(self):
|
def testWrongPassword(self):
|
||||||
"""
|
|
||||||
"If the password supplied does not match the password expected by the server,
|
|
||||||
then the server SHOULD send ERR_PASSWDMISMATCH and MUST close the connection
|
|
||||||
with ERROR."
|
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/172
|
|
||||||
"""
|
|
||||||
self.addClient()
|
self.addClient()
|
||||||
self.sendLine(1, "PASS {}".format(self.password + "garbage"))
|
self.sendLine(1, "PASS {}".format(self.password + "garbage"))
|
||||||
self.sendLine(1, "NICK foo")
|
self.sendLine(1, "NICK foo")
|
||||||
@ -54,13 +46,6 @@ class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertNotEqual(
|
self.assertNotEqual(
|
||||||
m.command, "001", msg="Got 001 after NICK+USER but incorrect PASS"
|
m.command, "001", msg="Got 001 after NICK+USER but incorrect PASS"
|
||||||
)
|
)
|
||||||
self.assertIn(m.command, {ERR_PASSWDMISMATCH, "ERROR"})
|
|
||||||
|
|
||||||
if m.command == "ERR_PASSWDMISMATCH":
|
|
||||||
m = self.getRegistrationMessage(1)
|
|
||||||
self.assertEqual(
|
|
||||||
m.command, "ERROR", msg="ERR_PASSWDMISMATCH not followed by ERROR."
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", strict=True)
|
@cases.mark_specifications("RFC1459", "RFC2812", strict=True)
|
||||||
def testPassAfterNickuser(self):
|
def testPassAfterNickuser(self):
|
||||||
@ -97,10 +82,6 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812")
|
@cases.mark_specifications("RFC2812")
|
||||||
@cases.xfailIfSoftware(["Charybdis", "Solanum"], "very flaky")
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["ircu2", "Nefarious", "snircd"], "ircu2 does not send ERROR"
|
|
||||||
)
|
|
||||||
def testQuitErrors(self):
|
def testQuitErrors(self):
|
||||||
"""“A client session is terminated with a quit message. The server
|
"""“A client session is terminated with a quit message. The server
|
||||||
acknowledges this by sending an ERROR message to the client.”
|
acknowledges this by sending an ERROR message to the client.”
|
||||||
@ -181,10 +162,6 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
|||||||
"neither got 001.",
|
"neither got 001.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["ircu2", "Nefarious", "ngIRCd"],
|
|
||||||
"uses a default value instead of ERR_NEEDMOREPARAMS",
|
|
||||||
)
|
|
||||||
def testEmptyRealname(self):
|
def testEmptyRealname(self):
|
||||||
"""
|
"""
|
||||||
Syntax:
|
Syntax:
|
||||||
@ -206,3 +183,60 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase):
|
|||||||
command=ERR_NEEDMOREPARAMS,
|
command=ERR_NEEDMOREPARAMS,
|
||||||
params=[StrRe(r"(\*|foo)"), "USER", ANYSTR],
|
params=[StrRe(r"(\*|foo)"), "USER", ANYSTR],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
def testIrc301CapLs(self):
|
||||||
|
"""
|
||||||
|
Current version:
|
||||||
|
|
||||||
|
"The LS subcommand is used to list the capabilities supported by the server.
|
||||||
|
The client should send an LS subcommand with no other arguments to solicit
|
||||||
|
a list of all capabilities."
|
||||||
|
|
||||||
|
"If a client has not indicated support for CAP LS 302 features,
|
||||||
|
the server MUST NOT send these new features to the client."
|
||||||
|
-- <https://ircv3.net/specs/core/capability-negotiation.html>
|
||||||
|
|
||||||
|
Before the v3.1 / v3.2 merge:
|
||||||
|
|
||||||
|
IRCv3.1: “The LS subcommand is used to list the capabilities
|
||||||
|
supported by the server. The client should send an LS subcommand with
|
||||||
|
no other arguments to solicit a list of all capabilities.”
|
||||||
|
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-ls-subcommand>
|
||||||
|
|
||||||
|
IRCv3.2: “Servers MUST NOT send messages described by this document if
|
||||||
|
the client only supports version 3.1.”
|
||||||
|
-- <http://ircv3.net/specs/core/capability-negotiation-3.2.html#version-in-cap-ls>
|
||||||
|
""" # noqa
|
||||||
|
self.addClient()
|
||||||
|
self.sendLine(1, "CAP LS")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertNotEqual(
|
||||||
|
m.params[2],
|
||||||
|
"*",
|
||||||
|
m,
|
||||||
|
fail_msg="Server replied with multi-line CAP LS to a "
|
||||||
|
"“CAP LS” (ie. IRCv3.1) request: {msg}",
|
||||||
|
)
|
||||||
|
self.assertFalse(
|
||||||
|
any("=" in cap for cap in m.params[2].split()),
|
||||||
|
"Server replied with a name-value capability in "
|
||||||
|
"CAP LS reply as a response to “CAP LS” (ie. IRCv3.1) "
|
||||||
|
"request: {}".format(m),
|
||||||
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("IRCv3")
|
||||||
|
def testEmptyCapList(self):
|
||||||
|
"""“If no capabilities are active, an empty parameter must be sent.”
|
||||||
|
-- <http://ircv3.net/specs/core/capability-negotiation-3.1.html#the-cap-list-subcommand>
|
||||||
|
""" # noqa
|
||||||
|
self.addClient()
|
||||||
|
self.sendLine(1, "CAP LIST")
|
||||||
|
m = self.getRegistrationMessage(1)
|
||||||
|
self.assertMessageMatch(
|
||||||
|
m,
|
||||||
|
command="CAP",
|
||||||
|
params=["*", "LIST", ""],
|
||||||
|
fail_msg="Sending “CAP LIST” as first message got a reply "
|
||||||
|
"that is not “CAP * LIST :”: {msg}",
|
||||||
|
)
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
`IRCv3 echo-message <https://ircv3.net/specs/extensions/echo-message>`_
|
<http://ircv3.net/specs/extensions/echo-message-3.2.html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
|
from irctest.basecontrollers import NotImplementedByController
|
||||||
from irctest.irc_utils.junkdrawer import random_name
|
from irctest.irc_utils.junkdrawer import random_name
|
||||||
from irctest.patma import ANYDICT
|
from irctest.patma import ANYDICT
|
||||||
|
|
||||||
@ -22,17 +23,36 @@ class EchoMessageTestCase(cases.BaseServerTestCase):
|
|||||||
@cases.mark_capabilities("echo-message")
|
@cases.mark_capabilities("echo-message")
|
||||||
def testEchoMessage(self, command, solo, server_time):
|
def testEchoMessage(self, command, solo, server_time):
|
||||||
"""<http://ircv3.net/specs/extensions/echo-message-3.2.html>"""
|
"""<http://ircv3.net/specs/extensions/echo-message-3.2.html>"""
|
||||||
capabilities = ["server-time"] if server_time else []
|
self.addClient()
|
||||||
|
self.sendLine(1, "CAP LS 302")
|
||||||
|
capabilities = self.getCapLs(1)
|
||||||
|
if "echo-message" not in capabilities:
|
||||||
|
raise NotImplementedByController("echo-message")
|
||||||
|
if server_time and "server-time" not in capabilities:
|
||||||
|
raise NotImplementedByController("server-time")
|
||||||
|
|
||||||
self.connectClient(
|
# TODO: check also without this
|
||||||
"baz",
|
self.sendLine(
|
||||||
capabilities=["echo-message", *capabilities],
|
1,
|
||||||
skip_if_cap_nak=True,
|
"CAP REQ :echo-message{}".format(" server-time" if server_time else ""),
|
||||||
)
|
)
|
||||||
|
self.getRegistrationMessage(1)
|
||||||
|
# TODO: Remove this one the trailing space issue is fixed in Charybdis
|
||||||
|
# and Mammon:
|
||||||
|
# self.assertMessageMatch(m, command='CAP',
|
||||||
|
# params=['*', 'ACK', 'echo-message'] +
|
||||||
|
# (['server-time'] if server_time else []),
|
||||||
|
# fail_msg='Did not ACK advertised capabilities: {msg}')
|
||||||
|
self.sendLine(1, "USER f * * :foo")
|
||||||
|
self.sendLine(1, "NICK baz")
|
||||||
|
self.sendLine(1, "CAP END")
|
||||||
|
self.skipToWelcome(1)
|
||||||
|
self.getMessages(1)
|
||||||
|
|
||||||
self.sendLine(1, "JOIN #chan")
|
self.sendLine(1, "JOIN #chan")
|
||||||
|
|
||||||
if not solo:
|
if not solo:
|
||||||
|
capabilities = ["server-time"] if server_time else None
|
||||||
self.connectClient("qux", capabilities=capabilities)
|
self.connectClient("qux", capabilities=capabilities)
|
||||||
self.sendLine(2, "JOIN #chan")
|
self.sendLine(2, "JOIN #chan")
|
||||||
|
|
||||||
|
@ -1,29 +0,0 @@
|
|||||||
"""
|
|
||||||
`Ergo <https://ergo.chat/>`-specific tests of NickServ.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
|
||||||
from irctest.numerics import RPL_YOUREOPER
|
|
||||||
|
|
||||||
|
|
||||||
class NickservTestCase(cases.BaseServerTestCase):
|
|
||||||
@cases.mark_specifications("Ergo")
|
|
||||||
def test_saregister(self):
|
|
||||||
self.connectClient("root", name="root")
|
|
||||||
self.sendLine("root", "OPER operuser operpassword")
|
|
||||||
self.assertIn(RPL_YOUREOPER, {msg.command for msg in self.getMessages("root")})
|
|
||||||
|
|
||||||
self.sendLine(
|
|
||||||
"root",
|
|
||||||
"PRIVMSG NickServ :SAREGISTER saregister_test saregistertestpassphrase",
|
|
||||||
)
|
|
||||||
self.getMessages("root")
|
|
||||||
|
|
||||||
# test that the account was registered
|
|
||||||
self.connectClient(
|
|
||||||
name="saregister_test",
|
|
||||||
nick="saregister_test",
|
|
||||||
capabilities=["sasl"],
|
|
||||||
account="saregister_test",
|
|
||||||
password="saregistertestpassphrase",
|
|
||||||
)
|
|
@ -1,12 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
`IRCv3 extended-join <https://ircv3.net/specs/extensions/extended-join>`_
|
<http://ircv3.net/specs/extensions/extended-join-3.1.html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
@cases.mark_services
|
||||||
class MetadataTestCase(cases.BaseServerTestCase):
|
class MetadataTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||||
def connectRegisteredClient(self, nick):
|
def connectRegisteredClient(self, nick):
|
||||||
self.addClient()
|
self.addClient()
|
||||||
self.sendLine(2, "CAP LS 302")
|
self.sendLine(2, "CAP LS 302")
|
||||||
@ -50,7 +50,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_capabilities("extended-join")
|
@cases.mark_capabilities("extended-join")
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
def testLoggedIn(self):
|
def testLoggedIn(self):
|
||||||
self.connectClient("foo", capabilities=["extended-join"], skip_if_cap_nak=True)
|
self.connectClient("foo", capabilities=["extended-join"], skip_if_cap_nak=True)
|
||||||
self.joinChannel(1, "#chan")
|
self.joinChannel(1, "#chan")
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
"""
|
|
||||||
The HELP and HELPOP command (`Modern <https://modern.ircdocs.horse/#help-message>`__)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import functools
|
|
||||||
import re
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from irctest import cases, runner
|
|
||||||
from irctest.numerics import (
|
|
||||||
ERR_HELPNOTFOUND,
|
|
||||||
ERR_UNKNOWNCOMMAND,
|
|
||||||
RPL_ENDOFHELP,
|
|
||||||
RPL_HELPSTART,
|
|
||||||
RPL_HELPTXT,
|
|
||||||
)
|
|
||||||
from irctest.patma import ANYSTR, StrRe
|
|
||||||
|
|
||||||
|
|
||||||
def with_xfails(f):
|
|
||||||
@functools.wraps(f)
|
|
||||||
def newf(self, command, *args, **kwargs):
|
|
||||||
if command == "HELP" and self.controller.software_name == "Bahamut":
|
|
||||||
raise runner.ImplementationChoice(
|
|
||||||
"fail because Bahamut forwards /HELP to HelpServ (but not /HELPOP)"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.controller.software_name in ("irc2", "ircu2", "ngIRCd"):
|
|
||||||
raise runner.ImplementationChoice(
|
|
||||||
"numerics in reply to /HELP and /HELPOP (uses NOTICE instead)"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.controller.software_name == "UnrealIRCd":
|
|
||||||
raise runner.ImplementationChoice(
|
|
||||||
"fails because Unreal uses custom numerics "
|
|
||||||
"https://github.com/unrealircd/unrealircd/pull/184"
|
|
||||||
)
|
|
||||||
|
|
||||||
return f(self, command, *args, **kwargs)
|
|
||||||
|
|
||||||
return newf
|
|
||||||
|
|
||||||
|
|
||||||
class HelpTestCase(cases.BaseServerTestCase):
|
|
||||||
def _assertValidHelp(self, messages, subject):
|
|
||||||
if subject != ANYSTR:
|
|
||||||
subject = StrRe("(?i)" + re.escape(subject))
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages[0],
|
|
||||||
command=RPL_HELPSTART,
|
|
||||||
params=["nick", subject, ANYSTR],
|
|
||||||
fail_msg=f"Expected {RPL_HELPSTART} (RPL_HELPSTART), got: {{msg}}",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages[-1],
|
|
||||||
command=RPL_ENDOFHELP,
|
|
||||||
params=["nick", subject, ANYSTR],
|
|
||||||
fail_msg=f"Expected {RPL_ENDOFHELP} (RPL_ENDOFHELP), got: {{msg}}",
|
|
||||||
)
|
|
||||||
|
|
||||||
for i in range(1, len(messages) - 1):
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages[i],
|
|
||||||
command=RPL_HELPTXT,
|
|
||||||
params=["nick", subject, ANYSTR],
|
|
||||||
fail_msg=f"Expected {RPL_HELPTXT} (RPL_HELPTXT), got: {{msg}}",
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("command", ["HELP", "HELPOP"])
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@with_xfails
|
|
||||||
def testHelpNoArg(self, command):
|
|
||||||
self.connectClient("nick")
|
|
||||||
self.sendLine(1, f"{command}")
|
|
||||||
|
|
||||||
messages = self.getMessages(1)
|
|
||||||
|
|
||||||
if messages[0].command == ERR_UNKNOWNCOMMAND:
|
|
||||||
raise runner.OptionalCommandNotSupported(command)
|
|
||||||
|
|
||||||
self._assertValidHelp(messages, ANYSTR)
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("command", ["HELP", "HELPOP"])
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@with_xfails
|
|
||||||
def testHelpPrivmsg(self, command):
|
|
||||||
self.connectClient("nick")
|
|
||||||
self.sendLine(1, f"{command} PRIVMSG")
|
|
||||||
messages = self.getMessages(1)
|
|
||||||
|
|
||||||
if messages[0].command == ERR_UNKNOWNCOMMAND:
|
|
||||||
raise runner.OptionalCommandNotSupported(command)
|
|
||||||
|
|
||||||
self._assertValidHelp(messages, "PRIVMSG")
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("command", ["HELP", "HELPOP"])
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@with_xfails
|
|
||||||
def testHelpUnknownSubject(self, command):
|
|
||||||
self.connectClient("nick")
|
|
||||||
self.sendLine(1, f"{command} THISISNOTACOMMAND")
|
|
||||||
messages = self.getMessages(1)
|
|
||||||
|
|
||||||
if messages[0].command == ERR_UNKNOWNCOMMAND:
|
|
||||||
raise runner.OptionalCommandNotSupported(command)
|
|
||||||
|
|
||||||
if messages[0].command == ERR_HELPNOTFOUND:
|
|
||||||
# Inspircd, Hybrid et al
|
|
||||||
self.assertEqual(len(messages), 1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages[0],
|
|
||||||
command=ERR_HELPNOTFOUND,
|
|
||||||
params=[
|
|
||||||
"nick",
|
|
||||||
StrRe(
|
|
||||||
"(?i)THISISNOTACOMMAND"
|
|
||||||
), # case-insensitive, for Hybrid and Plexus4 (but not Chary et al)
|
|
||||||
ANYSTR,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Unrealircd
|
|
||||||
self._assertValidHelp(messages, ANYSTR)
|
|
@ -1,118 +0,0 @@
|
|||||||
"""
|
|
||||||
The INFO command (`RFC 1459
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.8>`__,
|
|
||||||
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.10>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#info-message>`__)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from irctest import cases
|
|
||||||
from irctest.numerics import ERR_NOSUCHSERVER, RPL_ENDOFINFO, RPL_INFO, RPL_YOUREOPER
|
|
||||||
from irctest.patma import ANYSTR
|
|
||||||
|
|
||||||
|
|
||||||
class InfoTestCase(cases.BaseServerTestCase):
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
|
||||||
def testInfo(self):
|
|
||||||
"""
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.8>
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.10>
|
|
||||||
|
|
||||||
"Upon receiving an INFO command, the given server will respond with zero or
|
|
||||||
more RPL_INFO replies, followed by one RPL_ENDOFINFO numeric"
|
|
||||||
-- <https://modern.ircdocs.horse/#info-message>
|
|
||||||
"""
|
|
||||||
self.connectClient("nick")
|
|
||||||
|
|
||||||
# Remote /INFO is oper-only on Unreal and ircu2
|
|
||||||
self.sendLine(1, "OPER operuser operpassword")
|
|
||||||
self.assertIn(
|
|
||||||
RPL_YOUREOPER,
|
|
||||||
[m.command for m in self.getMessages(1)],
|
|
||||||
fail_msg="OPER failed",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "INFO")
|
|
||||||
|
|
||||||
messages = self.getMessages(1)
|
|
||||||
last_message = messages.pop()
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
last_message, command=RPL_ENDOFINFO, params=["nick", ANYSTR]
|
|
||||||
)
|
|
||||||
|
|
||||||
for message in messages:
|
|
||||||
self.assertMessageMatch(message, command=RPL_INFO, params=["nick", ANYSTR])
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"target",
|
|
||||||
["My.Little.Server", "*Little*", "nick"],
|
|
||||||
ids=["target-server", "target-wildcard", "target-nick"],
|
|
||||||
)
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", deprecated=True)
|
|
||||||
def testInfoTarget(self, target):
|
|
||||||
"""
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.8>
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.10>
|
|
||||||
|
|
||||||
"Upon receiving an INFO command, the given server will respond with zero or
|
|
||||||
more RPL_INFO replies, followed by one RPL_ENDOFINFO numeric"
|
|
||||||
-- <https://modern.ircdocs.horse/#info-message>
|
|
||||||
"""
|
|
||||||
self.connectClient("nick")
|
|
||||||
|
|
||||||
# Remote /INFO is oper-only on Unreal and ircu2
|
|
||||||
self.sendLine(1, "OPER operuser operpassword")
|
|
||||||
self.assertIn(
|
|
||||||
RPL_YOUREOPER,
|
|
||||||
[m.command for m in self.getMessages(1)],
|
|
||||||
fail_msg="OPER failed",
|
|
||||||
)
|
|
||||||
|
|
||||||
if target:
|
|
||||||
self.sendLine(1, "INFO My.Little.Server")
|
|
||||||
else:
|
|
||||||
self.sendLine(1, "INFO")
|
|
||||||
|
|
||||||
messages = self.getMessages(1)
|
|
||||||
last_message = messages.pop()
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
last_message, command=RPL_ENDOFINFO, params=["nick", ANYSTR]
|
|
||||||
)
|
|
||||||
|
|
||||||
for message in messages:
|
|
||||||
self.assertMessageMatch(message, command=RPL_INFO, params=["nick", ANYSTR])
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("target", ["invalid.server.example", "invalidserver"])
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", deprecated=True)
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["Ergo"], "does not apply to Ergo, which ignores the optional <target> argument"
|
|
||||||
)
|
|
||||||
def testInfoNosuchserver(self, target):
|
|
||||||
"""
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.8>
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.10>
|
|
||||||
|
|
||||||
"Upon receiving an INFO command, the given server will respond with zero or
|
|
||||||
more RPL_INFO replies, followed by one RPL_ENDOFINFO numeric"
|
|
||||||
-- <https://modern.ircdocs.horse/#info-message>
|
|
||||||
"""
|
|
||||||
self.connectClient("nick")
|
|
||||||
|
|
||||||
# Remote /INFO is oper-only on Unreal and ircu2
|
|
||||||
self.sendLine(1, "OPER operuser operpassword")
|
|
||||||
self.assertIn(
|
|
||||||
RPL_YOUREOPER,
|
|
||||||
[m.command for m in self.getMessages(1)],
|
|
||||||
fail_msg="OPER failed",
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, f"INFO {target}")
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=ERR_NOSUCHSERVER,
|
|
||||||
params=["nick", target, ANYSTR],
|
|
||||||
)
|
|
@ -1,18 +1,9 @@
|
|||||||
"""
|
|
||||||
The INVITE command (`RFC 1459
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.7>`__,
|
|
||||||
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.7>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#invite-message>`__)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from irctest import cases, runner
|
from irctest import cases
|
||||||
from irctest.numerics import (
|
from irctest.numerics import (
|
||||||
ERR_BANNEDFROMCHAN,
|
|
||||||
ERR_CHANOPRIVSNEEDED,
|
ERR_CHANOPRIVSNEEDED,
|
||||||
ERR_INVITEONLYCHAN,
|
ERR_INVITEONLYCHAN,
|
||||||
ERR_NEEDMOREPARAMS,
|
|
||||||
ERR_NOSUCHNICK,
|
ERR_NOSUCHNICK,
|
||||||
ERR_NOTONCHANNEL,
|
ERR_NOTONCHANNEL,
|
||||||
ERR_USERONCHANNEL,
|
ERR_USERONCHANNEL,
|
||||||
@ -118,7 +109,7 @@ class InviteTestCase(cases.BaseServerTestCase):
|
|||||||
"got this instead: {msg}",
|
"got this instead: {msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def _testInvite(self, opped, invite_only):
|
def _testInvite(self, opped, invite_only, modern):
|
||||||
"""
|
"""
|
||||||
"Only the user inviting and the user being invited will receive
|
"Only the user inviting and the user being invited will receive
|
||||||
notification of the invitation."
|
notification of the invitation."
|
||||||
@ -171,14 +162,23 @@ class InviteTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.sendLine(1, "INVITE bar #chan")
|
self.sendLine(1, "INVITE bar #chan")
|
||||||
self.assertMessageMatch(
|
if modern:
|
||||||
self.getMessage(1),
|
self.assertMessageMatch(
|
||||||
command=RPL_INVITING,
|
self.getMessage(1),
|
||||||
params=["foo", "bar", "#chan"],
|
command=RPL_INVITING,
|
||||||
fail_msg=f"After “foo” invited “bar” to a channel, “foo” should have "
|
params=["foo", "bar", "#chan"],
|
||||||
f"received “{RPL_INVITING} foo #chan bar” but got this instead: "
|
fail_msg=f"After “foo” invited “bar” to a channel, “foo” should have "
|
||||||
f"{{msg}}",
|
f"received “{RPL_INVITING} foo #chan bar” but got this instead: "
|
||||||
)
|
f"{{msg}}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(1),
|
||||||
|
command=RPL_INVITING,
|
||||||
|
params=["#chan", "bar"],
|
||||||
|
fail_msg=f"After “foo” invited “bar” to a channel, “foo” should have "
|
||||||
|
f"received “{RPL_INVITING} #chan bar” but got this instead: {{msg}}",
|
||||||
|
)
|
||||||
|
|
||||||
messages = self.getMessages(2)
|
messages = self.getMessages(2)
|
||||||
self.assertNotEqual(
|
self.assertNotEqual(
|
||||||
@ -196,17 +196,24 @@ class InviteTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@pytest.mark.parametrize("invite_only", [True, False])
|
@pytest.mark.parametrize("invite_only", [True, False])
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testInvite(self, invite_only):
|
def testInviteModern(self, invite_only):
|
||||||
self._testInvite(opped=True, invite_only=invite_only)
|
self._testInvite(opped=True, invite_only=invite_only, modern=True)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern", strict=True)
|
@pytest.mark.parametrize("invite_only", [True, False])
|
||||||
@cases.xfailIfSoftware(
|
@cases.mark_specifications("RFC1459", "RFC2812", deprecated=True)
|
||||||
["Hybrid", "Plexus4"], "the only strict test that Hybrid fails"
|
def testInviteRfc(self, invite_only):
|
||||||
)
|
self._testInvite(opped=True, invite_only=invite_only, modern=False)
|
||||||
def testInviteUnopped(self):
|
|
||||||
|
@cases.mark_specifications("Modern", strict=True)
|
||||||
|
def testInviteUnoppedModern(self):
|
||||||
"""Tests invites from unopped users on not-invite-only chans."""
|
"""Tests invites from unopped users on not-invite-only chans."""
|
||||||
self._testInvite(opped=False, invite_only=False)
|
self._testInvite(opped=False, invite_only=False, modern=True)
|
||||||
|
|
||||||
|
@cases.mark_specifications("RFC1459", "RFC2812", deprecated=True, strict=True)
|
||||||
|
def testInviteUnoppedRfc(self, opped, invite_only):
|
||||||
|
"""Tests invites from unopped users on not-invite-only chans."""
|
||||||
|
self._testInvite(opped=False, invite_only=False, modern=False)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812", "Modern")
|
@cases.mark_specifications("RFC2812", "Modern")
|
||||||
def testInviteNoNotificationForOtherMembers(self):
|
def testInviteNoNotificationForOtherMembers(self):
|
||||||
@ -240,13 +247,7 @@ class InviteTestCase(cases.BaseServerTestCase):
|
|||||||
"were notified: {got}",
|
"were notified: {got}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
def _testInviteInviteOnly(self, modern):
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["Plexus4"],
|
|
||||||
"Plexus4 allows non-op to invite if (and only if) the channel is not "
|
|
||||||
"invite-only",
|
|
||||||
)
|
|
||||||
def testInviteInviteOnly(self):
|
|
||||||
"""
|
"""
|
||||||
"To invite a user to a channel which is invite only (MODE
|
"To invite a user to a channel which is invite only (MODE
|
||||||
+i), the client sending the invite must be recognised as being a
|
+i), the client sending the invite must be recognised as being a
|
||||||
@ -286,17 +287,35 @@ class InviteTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.sendLine(1, "INVITE bar #chan")
|
self.sendLine(1, "INVITE bar #chan")
|
||||||
self.assertMessageMatch(
|
if modern:
|
||||||
self.getMessage(1),
|
self.assertMessageMatch(
|
||||||
command=ERR_CHANOPRIVSNEEDED,
|
self.getMessage(1),
|
||||||
params=["foo", "#chan", ANYSTR],
|
command=ERR_CHANOPRIVSNEEDED,
|
||||||
fail_msg=f"After “foo” invited “bar” to a channel to an invite-only "
|
params=["foo", "#chan", ANYSTR],
|
||||||
f"channel without being opped, “foo” should have received "
|
fail_msg=f"After “foo” invited “bar” to a channel to an invite-only "
|
||||||
f"“{ERR_CHANOPRIVSNEEDED} foo #chan :*” but got this instead: {{msg}}",
|
f"channel without being opped, “foo” should have received "
|
||||||
)
|
f"“{ERR_CHANOPRIVSNEEDED} foo #chan :*” but got this instead: {{msg}}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(1),
|
||||||
|
command=ERR_CHANOPRIVSNEEDED,
|
||||||
|
params=["#chan", ANYSTR],
|
||||||
|
fail_msg=f"After “foo” invited “bar” to a channel to an invite-only "
|
||||||
|
f"channel without being opped, “foo” should have received "
|
||||||
|
f"“{ERR_CHANOPRIVSNEEDED} #chan :*” but got this instead: {{msg}}",
|
||||||
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("Modern")
|
||||||
|
def testInviteInviteOnlyModern(self):
|
||||||
|
self._testInviteInviteOnly(modern=True)
|
||||||
|
|
||||||
|
@cases.mark_specifications("RFC1459", "RFC2812", deprecated=True)
|
||||||
|
def testInviteInviteOnlyRfc(self):
|
||||||
|
self._testInviteInviteOnly(modern=False)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812", "Modern")
|
@cases.mark_specifications("RFC2812", "Modern")
|
||||||
def testInviteOnlyFromUsersInChannel(self):
|
def _testInviteOnlyFromUsersInChannel(self, modern):
|
||||||
"""
|
"""
|
||||||
"if the channel exists, only members of the channel are allowed
|
"if the channel exists, only members of the channel are allowed
|
||||||
to invite other users"
|
to invite other users"
|
||||||
@ -329,15 +348,26 @@ class InviteTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(3)
|
self.getMessages(3)
|
||||||
|
|
||||||
self.sendLine(1, "INVITE bar #chan")
|
self.sendLine(1, "INVITE bar #chan")
|
||||||
self.assertMessageMatch(
|
if modern:
|
||||||
self.getMessage(1),
|
self.assertMessageMatch(
|
||||||
command=ERR_NOTONCHANNEL,
|
self.getMessage(1),
|
||||||
params=["foo", "#chan", ANYSTR],
|
command=ERR_NOTONCHANNEL,
|
||||||
fail_msg=f"After “foo” invited “bar” to a channel it is not on "
|
params=["foo", "#chan", ANYSTR],
|
||||||
f"#chan, “foo” should have received "
|
fail_msg=f"After “foo” invited “bar” to a channel it is not on "
|
||||||
f"“ERR_NOTONCHANNEL ({ERR_NOTONCHANNEL}) foo #chan :*” but "
|
f"#chan, “foo” should have received "
|
||||||
f"got this instead: {{msg}}",
|
f"“ERR_NOTONCHANNEL ({ERR_NOTONCHANNEL}) foo #chan :*” but "
|
||||||
)
|
f"got this instead: {{msg}}",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage(1),
|
||||||
|
command=ERR_NOTONCHANNEL,
|
||||||
|
params=["#chan", ANYSTR],
|
||||||
|
fail_msg=f"After “foo” invited “bar” to a channel it is not on "
|
||||||
|
f"#chan, “foo” should have received "
|
||||||
|
f"“ERR_NOTONCHANNEL ({ERR_NOTONCHANNEL}) #chan :*” but "
|
||||||
|
f"got this instead: {{msg}}",
|
||||||
|
)
|
||||||
|
|
||||||
messages = self.getMessages(2)
|
messages = self.getMessages(2)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@ -347,6 +377,14 @@ class InviteTestCase(cases.BaseServerTestCase):
|
|||||||
"not in #chan, “bar” received something.",
|
"not in #chan, “bar” received something.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("Modern")
|
||||||
|
def testInviteOnlyFromUsersInChannelModern(self):
|
||||||
|
self._testInviteOnlyFromUsersInChannel(modern=True)
|
||||||
|
|
||||||
|
@cases.mark_specifications("RFC2812", deprecated=True)
|
||||||
|
def testInviteOnlyFromUsersInChannelRfc(self):
|
||||||
|
self._testInviteOnlyFromUsersInChannel(modern=False)
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
@cases.mark_specifications("Modern")
|
||||||
def testInviteAlreadyInChannel(self):
|
def testInviteAlreadyInChannel(self):
|
||||||
"""
|
"""
|
||||||
@ -360,8 +398,8 @@ class InviteTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
|
|
||||||
self.sendLine(1, "JOIN #chan")
|
self.sendLine(1, "JOIN #chan")
|
||||||
self.getMessages(1)
|
|
||||||
self.sendLine(2, "JOIN #chan")
|
self.sendLine(2, "JOIN #chan")
|
||||||
|
self.getMessages(1)
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
|
|
||||||
@ -372,124 +410,3 @@ class InviteTestCase(cases.BaseServerTestCase):
|
|||||||
command=ERR_USERONCHANNEL,
|
command=ERR_USERONCHANNEL,
|
||||||
params=["foo", "bar", "#chan", ANYSTR],
|
params=["foo", "bar", "#chan", ANYSTR],
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812", "Modern")
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["ircu2"],
|
|
||||||
"Uses 346/347 instead of 336/337 to reply to INVITE "
|
|
||||||
"https://github.com/UndernetIRC/ircu2/pull/20",
|
|
||||||
)
|
|
||||||
def testInviteList(self):
|
|
||||||
self.connectClient("foo")
|
|
||||||
self.connectClient("bar")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.getMessages(2)
|
|
||||||
|
|
||||||
self.sendLine(1, "JOIN #chan")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "INVITE bar #chan")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.getMessages(2)
|
|
||||||
|
|
||||||
self.sendLine(2, "INVITE")
|
|
||||||
m = self.getMessage(2)
|
|
||||||
if m.command == ERR_NEEDMOREPARAMS:
|
|
||||||
raise runner.OptionalExtensionNotSupported("INVITE with no parameter")
|
|
||||||
if m.command != "337":
|
|
||||||
# Hybrid always sends an empty list; so skip this.
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="336",
|
|
||||||
params=["bar", "#chan"],
|
|
||||||
)
|
|
||||||
m = self.getMessage(2)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="337",
|
|
||||||
params=["bar", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_isupport("INVEX")
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testInvexList(self):
|
|
||||||
self.connectClient("foo")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
if "INVEX" in self.server_support:
|
|
||||||
invex = self.server_support.get("INVEX") or "I"
|
|
||||||
else:
|
|
||||||
raise runner.IsupportTokenNotSupported("INVEX")
|
|
||||||
|
|
||||||
self.sendLine(1, "JOIN #chan")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, f"MODE #chan +{invex} bar!*@*")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, f"MODE #chan +{invex}")
|
|
||||||
m = self.getMessage(1)
|
|
||||||
if len(m.params) == 3:
|
|
||||||
# Old format
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="346",
|
|
||||||
params=["foo", "#chan", "bar!*@*"],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="346",
|
|
||||||
params=[
|
|
||||||
"foo",
|
|
||||||
"#chan",
|
|
||||||
"bar!*@*",
|
|
||||||
StrRe("foo(!.*@.*)?"),
|
|
||||||
StrRe("[0-9]+"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command="347",
|
|
||||||
params=["foo", "#chan", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Ergo")
|
|
||||||
def testInviteExemptsFromBan(self):
|
|
||||||
# regression test for ergochat/ergo#1876;
|
|
||||||
# INVITE should override a +b ban
|
|
||||||
self.connectClient("alice", name="alice")
|
|
||||||
self.joinChannel("alice", "#alice")
|
|
||||||
self.sendLine("alice", "MODE #alice +b bob!*@*")
|
|
||||||
result = {msg.command for msg in self.getMessages("alice")}
|
|
||||||
self.assertIn("MODE", result)
|
|
||||||
|
|
||||||
self.connectClient("bob", name="bob")
|
|
||||||
self.sendLine("bob", "JOIN #alice")
|
|
||||||
result = {msg.command for msg in self.getMessages("bob")}
|
|
||||||
self.assertIn(ERR_BANNEDFROMCHAN, result)
|
|
||||||
self.assertNotIn("JOIN", result)
|
|
||||||
|
|
||||||
self.sendLine("alice", "INVITE bob #alice")
|
|
||||||
result = {msg.command for msg in self.getMessages("alice")}
|
|
||||||
self.assertIn(RPL_INVITING, result)
|
|
||||||
self.assertNotIn(ERR_USERONCHANNEL, result)
|
|
||||||
|
|
||||||
result = {msg.command for msg in self.getMessages("bob")}
|
|
||||||
self.assertIn("INVITE", result)
|
|
||||||
|
|
||||||
self.sendLine("bob", "JOIN #alice")
|
|
||||||
result = {msg.command for msg in self.getMessages("bob")}
|
|
||||||
self.assertNotIn(ERR_BANNEDFROMCHAN, result)
|
|
||||||
self.assertIn("JOIN", result)
|
|
||||||
|
|
||||||
self.sendLine("alice", "KICK #alice bob")
|
|
||||||
self.getMessages("alice")
|
|
||||||
result = {msg.command for msg in self.getMessages("bob")}
|
|
||||||
self.assertIn("KICK", result)
|
|
||||||
|
|
||||||
# INVITE gets "used up" after one JOIN
|
|
||||||
self.sendLine("bob", "JOIN #alice")
|
|
||||||
result = {msg.command for msg in self.getMessages("bob")}
|
|
||||||
self.assertIn(ERR_BANNEDFROMCHAN, result)
|
|
||||||
self.assertNotIn("JOIN", result)
|
|
||||||
|
@ -1,72 +1,9 @@
|
|||||||
"""
|
|
||||||
RPL_ISUPPORT: `format <https://modern.ircdocs.horse/#rplisupport-005>`__
|
|
||||||
and various `tokens <https://modern.ircdocs.horse/#rplisupport-parameters>`__
|
|
||||||
"""
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from irctest import cases, runner
|
from irctest import cases, runner
|
||||||
|
|
||||||
|
|
||||||
class IsupportTestCase(cases.BaseServerTestCase):
|
class IsupportTestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@cases.mark_isupport("PREFIX")
|
|
||||||
def testPrefix(self):
|
|
||||||
"""https://modern.ircdocs.horse/#prefix-parameter"""
|
|
||||||
self.connectClient("foo")
|
|
||||||
|
|
||||||
if "PREFIX" not in self.server_support:
|
|
||||||
raise runner.IsupportTokenNotSupported("PREFIX")
|
|
||||||
|
|
||||||
if self.server_support["PREFIX"] == "":
|
|
||||||
# "The value is OPTIONAL and when it is not specified indicates that no
|
|
||||||
# prefixes are supported."
|
|
||||||
return
|
|
||||||
|
|
||||||
m = re.match(
|
|
||||||
r"\((?P<modes>[a-zA-Z]+)\)(?P<prefixes>\S+)", self.server_support["PREFIX"]
|
|
||||||
)
|
|
||||||
self.assertTrue(
|
|
||||||
m,
|
|
||||||
f"PREFIX={self.server_support['PREFIX']} does not have the expected "
|
|
||||||
f"format.",
|
|
||||||
)
|
|
||||||
|
|
||||||
modes = m.group("modes")
|
|
||||||
prefixes = m.group("prefixes")
|
|
||||||
|
|
||||||
# "There is a one-to-one mapping between prefixes and channel modes."
|
|
||||||
self.assertEqual(
|
|
||||||
len(modes), len(prefixes), "Mismatched length of prefix and channel modes."
|
|
||||||
)
|
|
||||||
|
|
||||||
# "The prefixes in this parameter are in descending order, from the prefix
|
|
||||||
# that gives the most privileges to the prefix that gives the least."
|
|
||||||
self.assertLess(modes.index("o"), modes.index("v"), "'o' is not before 'v'")
|
|
||||||
if "h" in modes:
|
|
||||||
self.assertLess(modes.index("o"), modes.index("h"), "'o' is not before 'h'")
|
|
||||||
self.assertLess(modes.index("h"), modes.index("v"), "'h' is not before 'v'")
|
|
||||||
if "q" in modes:
|
|
||||||
self.assertLess(modes.index("q"), modes.index("o"), "'q' is not before 'o'")
|
|
||||||
|
|
||||||
# Not technically in the spec, but it would be very confusing not to follow
|
|
||||||
# these conventions.
|
|
||||||
mode_to_prefix = dict(zip(modes, prefixes))
|
|
||||||
self.assertEqual(mode_to_prefix["o"], "@", "Prefix char for mode +o is not @")
|
|
||||||
self.assertEqual(mode_to_prefix["v"], "+", "Prefix char for mode +v is not +")
|
|
||||||
if "h" in modes:
|
|
||||||
self.assertEqual(
|
|
||||||
mode_to_prefix["h"], "%", "Prefix char for mode +h is not %"
|
|
||||||
)
|
|
||||||
if "q" in modes:
|
|
||||||
self.assertEqual(
|
|
||||||
mode_to_prefix["q"], "~", "Prefix char for mode +q is not ~"
|
|
||||||
)
|
|
||||||
if "a" in modes:
|
|
||||||
self.assertEqual(
|
|
||||||
mode_to_prefix["a"], "&", "Prefix char for mode +a is not &"
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern", "ircdocs")
|
@cases.mark_specifications("Modern", "ircdocs")
|
||||||
@cases.mark_isupport("TARGMAX")
|
@cases.mark_isupport("TARGMAX")
|
||||||
def testTargmax(self):
|
def testTargmax(self):
|
||||||
@ -80,7 +17,7 @@ class IsupportTestCase(cases.BaseServerTestCase):
|
|||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
|
|
||||||
if "TARGMAX" not in self.server_support:
|
if "TARGMAX" not in self.server_support:
|
||||||
raise runner.IsupportTokenNotSupported("TARGMAX")
|
raise runner.NotImplementedByController("TARGMAX")
|
||||||
|
|
||||||
parts = self.server_support["TARGMAX"].split(",")
|
parts = self.server_support["TARGMAX"].split(",")
|
||||||
for part in parts:
|
for part in parts:
|
||||||
|
@ -1,30 +1,5 @@
|
|||||||
"""
|
from irctest import cases
|
||||||
The JOIN command (`RFC 1459
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.1>`__,
|
|
||||||
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.1>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#join-message>`__)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases, runner
|
|
||||||
from irctest.irc_utils import ambiguities
|
from irctest.irc_utils import ambiguities
|
||||||
from irctest.numerics import (
|
|
||||||
ERR_BADCHANMASK,
|
|
||||||
ERR_FORBIDDENCHANNEL,
|
|
||||||
ERR_NOSUCHCHANNEL,
|
|
||||||
RPL_ENDOFNAMES,
|
|
||||||
RPL_NAMREPLY,
|
|
||||||
)
|
|
||||||
from irctest.patma import ANYSTR, StrRe
|
|
||||||
|
|
||||||
ERR_BADCHANNAME = "479" # Hybrid only, and conflicts with others
|
|
||||||
|
|
||||||
|
|
||||||
JOIN_ERROR_NUMERICS = {
|
|
||||||
ERR_BADCHANMASK,
|
|
||||||
ERR_NOSUCHCHANNEL,
|
|
||||||
ERR_FORBIDDENCHANNEL,
|
|
||||||
ERR_BADCHANNAME,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class JoinTestCase(cases.BaseServerTestCase):
|
class JoinTestCase(cases.BaseServerTestCase):
|
||||||
@ -44,22 +19,13 @@ class JoinTestCase(cases.BaseServerTestCase):
|
|||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.sendLine(1, "JOIN #chan")
|
self.sendLine(1, "JOIN #chan")
|
||||||
received_commands = {m.command for m in self.getMessages(1)}
|
received_commands = {m.command for m in self.getMessages(1)}
|
||||||
expected_commands = {RPL_NAMREPLY, RPL_ENDOFNAMES, "JOIN"}
|
expected_commands = {"353", "366"} # RPL_NAMREPLY # RPL_ENDOFNAMES
|
||||||
acceptable_commands = expected_commands | {"MODE"}
|
self.assertTrue(
|
||||||
self.assertLessEqual( # set inclusion
|
expected_commands.issubset(received_commands),
|
||||||
expected_commands,
|
|
||||||
received_commands,
|
|
||||||
"Server sent {} commands, but at least {} were expected.".format(
|
"Server sent {} commands, but at least {} were expected.".format(
|
||||||
received_commands, expected_commands
|
received_commands, expected_commands
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.assertLessEqual( # ditto
|
|
||||||
received_commands,
|
|
||||||
acceptable_commands,
|
|
||||||
"Server sent {} commands, but only {} were expected.".format(
|
|
||||||
received_commands, acceptable_commands
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812")
|
@cases.mark_specifications("RFC2812")
|
||||||
def testJoinNamreply(self):
|
def testJoinNamreply(self):
|
||||||
@ -144,95 +110,3 @@ class JoinTestCase(cases.BaseServerTestCase):
|
|||||||
'"foo" with an optional "+" or "@" prefix, but got: '
|
'"foo" with an optional "+" or "@" prefix, but got: '
|
||||||
"{msg}",
|
"{msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
def testJoinPartiallyInvalid(self):
|
|
||||||
"""TODO: specify this in Modern"""
|
|
||||||
self.connectClient("foo")
|
|
||||||
if int(self.targmax.get("JOIN") or "4") < 2:
|
|
||||||
raise runner.OptionalExtensionNotSupported("multi-channel JOIN")
|
|
||||||
|
|
||||||
self.sendLine(1, "JOIN #valid,inv@lid")
|
|
||||||
messages = self.getMessages(1)
|
|
||||||
received_commands = {m.command for m in messages}
|
|
||||||
expected_commands = {RPL_NAMREPLY, RPL_ENDOFNAMES, "JOIN"}
|
|
||||||
acceptable_commands = expected_commands | JOIN_ERROR_NUMERICS | {"MODE"}
|
|
||||||
self.assertLessEqual(
|
|
||||||
expected_commands,
|
|
||||||
received_commands,
|
|
||||||
"Server sent {} commands, but at least {} were expected.".format(
|
|
||||||
received_commands, expected_commands
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertLessEqual(
|
|
||||||
received_commands,
|
|
||||||
acceptable_commands,
|
|
||||||
"Server sent {} commands, but only {} were expected.".format(
|
|
||||||
received_commands, acceptable_commands
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
nb_errors = 0
|
|
||||||
for m in messages:
|
|
||||||
if m.command in JOIN_ERROR_NUMERICS:
|
|
||||||
nb_errors += 1
|
|
||||||
self.assertMessageMatch(m, params=["foo", "inv@lid", ANYSTR])
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
nb_errors,
|
|
||||||
1,
|
|
||||||
fail_msg="Expected 1 error when joining channels '#valid' and 'inv@lid', "
|
|
||||||
"got {got}",
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_capabilities("batch", "labeled-response")
|
|
||||||
def testJoinPartiallyInvalidLabeledResponse(self):
|
|
||||||
"""TODO: specify this in Modern"""
|
|
||||||
self.connectClient(
|
|
||||||
"foo", capabilities=["batch", "labeled-response"], skip_if_cap_nak=True
|
|
||||||
)
|
|
||||||
if int(self.targmax.get("JOIN") or "4") < 2:
|
|
||||||
raise runner.OptionalExtensionNotSupported("multi-channel JOIN")
|
|
||||||
|
|
||||||
self.sendLine(1, "@label=label1 JOIN #valid,inv@lid")
|
|
||||||
messages = self.getMessages(1)
|
|
||||||
|
|
||||||
first_msg = messages.pop(0)
|
|
||||||
last_msg = messages.pop(-1)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
first_msg, command="BATCH", params=[StrRe(r"\+.*"), "labeled-response"]
|
|
||||||
)
|
|
||||||
batch_id = first_msg.params[0][1:]
|
|
||||||
self.assertMessageMatch(last_msg, command="BATCH", params=["-" + batch_id])
|
|
||||||
|
|
||||||
received_commands = {m.command for m in messages}
|
|
||||||
expected_commands = {RPL_NAMREPLY, RPL_ENDOFNAMES, "JOIN"}
|
|
||||||
acceptable_commands = expected_commands | JOIN_ERROR_NUMERICS | {"MODE"}
|
|
||||||
self.assertLessEqual(
|
|
||||||
expected_commands,
|
|
||||||
received_commands,
|
|
||||||
"Server sent {} commands, but at least {} were expected.".format(
|
|
||||||
received_commands, expected_commands
|
|
||||||
),
|
|
||||||
)
|
|
||||||
self.assertLessEqual(
|
|
||||||
received_commands,
|
|
||||||
acceptable_commands,
|
|
||||||
"Server sent {} commands, but only {} were expected.".format(
|
|
||||||
received_commands, acceptable_commands
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
nb_errors = 0
|
|
||||||
for m in messages:
|
|
||||||
self.assertIn("batch", m.tags)
|
|
||||||
self.assertEqual(m.tags["batch"], batch_id)
|
|
||||||
if m.command in JOIN_ERROR_NUMERICS:
|
|
||||||
nb_errors += 1
|
|
||||||
self.assertMessageMatch(m, params=["foo", "inv@lid", ANYSTR])
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
nb_errors,
|
|
||||||
1,
|
|
||||||
fail_msg="Expected 1 error when joining channels '#valid' and 'inv@lid', "
|
|
||||||
"got {got}",
|
|
||||||
)
|
|
||||||
|
@ -1,10 +1,3 @@
|
|||||||
"""
|
|
||||||
The KICK command (`RFC 1459
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.1>`__,
|
|
||||||
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#kick-message>`__)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from irctest import cases, client_mock, runner
|
from irctest import cases, client_mock, runner
|
||||||
@ -96,10 +89,6 @@ class KickTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertMessageMatch(m3, command="KICK", params=["#chan", "bar", ANYSTR])
|
self.assertMessageMatch(m3, command="KICK", params=["#chan", "bar", ANYSTR])
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812")
|
@cases.mark_specifications("RFC2812")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["Charybdis", "ircu2", "irc2", "Solanum"],
|
|
||||||
"uses the nick of the kickee rather than the kicker.",
|
|
||||||
)
|
|
||||||
def testKickDefaultComment(self):
|
def testKickDefaultComment(self):
|
||||||
"""
|
"""
|
||||||
"If a "comment" is
|
"If a "comment" is
|
||||||
@ -230,8 +219,13 @@ class KickTestCase(cases.BaseServerTestCase):
|
|||||||
self.connectClient("qux")
|
self.connectClient("qux")
|
||||||
self.joinChannel(4, "#chan")
|
self.joinChannel(4, "#chan")
|
||||||
|
|
||||||
if self.targmax.get("KICK", "1") == "1":
|
targmax = dict(
|
||||||
raise runner.OptionalExtensionNotSupported("Multi-target KICK")
|
item.split(":", 1)
|
||||||
|
for item in self.server_support.get("TARGMAX", "").split(",")
|
||||||
|
if item
|
||||||
|
)
|
||||||
|
if targmax.get("KICK", "1") == "1":
|
||||||
|
raise runner.NotImplementedByController("Multi-target KICK")
|
||||||
|
|
||||||
# TODO: check foo is an operator
|
# TODO: check foo is an operator
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
`IRCv3 labeled-response <https://ircv3.net/specs/extensions/labeled-response>`_
|
|
||||||
|
|
||||||
This specification is a little hard to test because all labels are optional;
|
This specification is a little hard to test because all labels are optional;
|
||||||
so there may be many false positives.
|
so there may be many false positives.
|
||||||
|
|
||||||
|
<https://ircv3.net/specs/extensions/labeled-response.html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
@ -10,12 +10,10 @@ import re
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import ERR_UNKNOWNCOMMAND
|
from irctest.patma import ANYDICT, AnyOptStr, NotStrRe, RemainingKeys, StrRe
|
||||||
from irctest.patma import ANYDICT, ANYOPTSTR, NotStrRe, RemainingKeys, StrRe
|
|
||||||
from irctest.runner import OptionalExtensionNotSupported
|
|
||||||
|
|
||||||
|
|
||||||
class LabeledResponsesTestCase(cases.BaseServerTestCase):
|
class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||||
@cases.mark_capabilities("echo-message", "batch", "labeled-response")
|
@cases.mark_capabilities("echo-message", "batch", "labeled-response")
|
||||||
def testLabeledPrivmsgResponsesToMultipleClients(self):
|
def testLabeledPrivmsgResponsesToMultipleClients(self):
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
@ -23,10 +21,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase):
|
|||||||
capabilities=["echo-message", "batch", "labeled-response"],
|
capabilities=["echo-message", "batch", "labeled-response"],
|
||||||
skip_if_cap_nak=True,
|
skip_if_cap_nak=True,
|
||||||
)
|
)
|
||||||
if int(self.targmax.get("PRIVMSG", "1") or "4") < 3:
|
|
||||||
raise OptionalExtensionNotSupported("PRIVMSG to multiple targets")
|
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
|
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
"bar",
|
"bar",
|
||||||
capabilities=["echo-message", "batch", "labeled-response"],
|
capabilities=["echo-message", "batch", "labeled-response"],
|
||||||
@ -303,7 +298,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase):
|
|||||||
tags={
|
tags={
|
||||||
"+draft/reply": msgid,
|
"+draft/reply": msgid,
|
||||||
"+draft/react": "l😃l",
|
"+draft/react": "l😃l",
|
||||||
RemainingKeys(NotStrRe("label")): ANYOPTSTR,
|
RemainingKeys(NotStrRe("label")): AnyOptStr(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
self.assertNotIn(
|
self.assertNotIn(
|
||||||
@ -371,7 +366,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase):
|
|||||||
tags={
|
tags={
|
||||||
"+draft/reply": msgid,
|
"+draft/reply": msgid,
|
||||||
"+draft/react": "l😃l",
|
"+draft/react": "l😃l",
|
||||||
RemainingKeys(NotStrRe("label")): ANYOPTSTR,
|
RemainingKeys(NotStrRe("label")): AnyOptStr(),
|
||||||
},
|
},
|
||||||
fail_msg="No TAGMSG received by the target after sending one out",
|
fail_msg="No TAGMSG received by the target after sending one out",
|
||||||
)
|
)
|
||||||
@ -507,19 +502,3 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase):
|
|||||||
ack = ms[0]
|
ack = ms[0]
|
||||||
|
|
||||||
self.assertMessageMatch(ack, command="ACK", tags={"label": "98765"})
|
self.assertMessageMatch(ack, command="ACK", tags={"label": "98765"})
|
||||||
|
|
||||||
@cases.mark_capabilities("labeled-response")
|
|
||||||
def testUnknownCommand(self):
|
|
||||||
self.connectClient(
|
|
||||||
"bar", capabilities=["batch", "labeled-response"], skip_if_cap_nak=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# this command doesn't exist, but the error response should still
|
|
||||||
# be labeled:
|
|
||||||
self.sendLine(1, "@label=deadbeef NONEXISTENT_COMMAND")
|
|
||||||
ms = self.getMessages(1)
|
|
||||||
self.assertEqual(len(ms), 1)
|
|
||||||
unknowncommand = ms[0]
|
|
||||||
self.assertMessageMatch(
|
|
||||||
unknowncommand, command=ERR_UNKNOWNCOMMAND, tags={"label": "deadbeef"}
|
|
||||||
)
|
|
||||||
|
@ -1,136 +0,0 @@
|
|||||||
from irctest import cases, runner
|
|
||||||
from irctest.numerics import ERR_UNKNOWNCOMMAND, RPL_ENDOFLINKS, RPL_LINKS
|
|
||||||
from irctest.patma import ANYSTR, StrRe
|
|
||||||
|
|
||||||
|
|
||||||
class LinksTestCase(cases.BaseServerTestCase):
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
|
||||||
def testLinksSingleServer(self):
|
|
||||||
"""
|
|
||||||
Only testing the parameter-less case.
|
|
||||||
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.3
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.5
|
|
||||||
https://github.com/ircdocs/modern-irc/pull/175
|
|
||||||
|
|
||||||
"
|
|
||||||
364 RPL_LINKS
|
|
||||||
"<mask> <server> :<hopcount> <server info>"
|
|
||||||
365 RPL_ENDOFLINKS
|
|
||||||
"<mask> :End of /LINKS list"
|
|
||||||
|
|
||||||
- In replying to the LINKS message, a server must send
|
|
||||||
replies back using the RPL_LINKS numeric and mark the
|
|
||||||
end of the list using an RPL_ENDOFLINKS reply.
|
|
||||||
"
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc1459#page-51
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#page-48
|
|
||||||
|
|
||||||
RPL_LINKS: "<client> * <server> :<hopcount> <server info>"
|
|
||||||
RPL_ENDOFLINKS: "<client> * :End of /LINKS list"
|
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/175/files
|
|
||||||
"""
|
|
||||||
self.connectClient("nick")
|
|
||||||
self.sendLine(1, "LINKS")
|
|
||||||
messages = self.getMessages(1)
|
|
||||||
if messages[0].command == ERR_UNKNOWNCOMMAND:
|
|
||||||
raise runner.OptionalCommandNotSupported("LINKS")
|
|
||||||
|
|
||||||
# Ignore '/LINKS has been disabled' from ircu2
|
|
||||||
messages = [m for m in messages if m.command != "NOTICE"]
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages.pop(-1),
|
|
||||||
command=RPL_ENDOFLINKS,
|
|
||||||
params=["nick", "*", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
if not messages:
|
|
||||||
# This server probably redacts links
|
|
||||||
return
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages[0],
|
|
||||||
command=RPL_LINKS,
|
|
||||||
params=[
|
|
||||||
"nick",
|
|
||||||
"My.Little.Server",
|
|
||||||
"My.Little.Server",
|
|
||||||
StrRe("0 (0042 )?test server"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
|
||||||
class ServicesLinksTestCase(cases.BaseServerTestCase):
|
|
||||||
# On every IRCd but Ergo, services are linked.
|
|
||||||
# Ergo does not implement LINKS at all, so this test is skipped.
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
|
||||||
def testLinksWithServices(self):
|
|
||||||
"""
|
|
||||||
Only testing the parameter-less case.
|
|
||||||
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.3
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.5
|
|
||||||
|
|
||||||
"
|
|
||||||
364 RPL_LINKS
|
|
||||||
"<mask> <server> :<hopcount> <server info>"
|
|
||||||
365 RPL_ENDOFLINKS
|
|
||||||
"<mask> :End of /LINKS list"
|
|
||||||
|
|
||||||
- In replying to the LINKS message, a server must send
|
|
||||||
replies back using the RPL_LINKS numeric and mark the
|
|
||||||
end of the list using an RPL_ENDOFLINKS reply.
|
|
||||||
"
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc1459#page-51
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#page-48
|
|
||||||
|
|
||||||
RPL_LINKS: "<client> * <server> :<hopcount> <server info>"
|
|
||||||
RPL_ENDOFLINKS: "<client> * :End of /LINKS list"
|
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/175/files
|
|
||||||
"""
|
|
||||||
self.connectClient("nick")
|
|
||||||
self.sendLine(1, "LINKS")
|
|
||||||
messages = self.getMessages(1)
|
|
||||||
|
|
||||||
if messages[0].command == ERR_UNKNOWNCOMMAND:
|
|
||||||
raise runner.OptionalCommandNotSupported("LINKS")
|
|
||||||
|
|
||||||
# Ignore '/LINKS has been disabled' from ircu2
|
|
||||||
messages = [m for m in messages if m.command != "NOTICE"]
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages.pop(-1),
|
|
||||||
command=RPL_ENDOFLINKS,
|
|
||||||
params=["nick", "*", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
if not messages:
|
|
||||||
# This server redacts links
|
|
||||||
return
|
|
||||||
|
|
||||||
messages.sort(key=lambda m: m.params[-1])
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages.pop(0),
|
|
||||||
command=RPL_LINKS,
|
|
||||||
params=[
|
|
||||||
"nick",
|
|
||||||
"My.Little.Server",
|
|
||||||
"My.Little.Server",
|
|
||||||
StrRe("0 (0042 )?test server"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
messages.pop(0),
|
|
||||||
command=RPL_LINKS,
|
|
||||||
params=[
|
|
||||||
"nick",
|
|
||||||
"services.example.org",
|
|
||||||
"My.Little.Server",
|
|
||||||
StrRe("1 .+"), # SID instead of description for Anope...
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(messages, [])
|
|
@ -1,71 +1,40 @@
|
|||||||
"""
|
from irctest import cases
|
||||||
The LIST command (`RFC 1459
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.6>`__,
|
|
||||||
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.6>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#list-message>`__)
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
from irctest import cases, runner
|
|
||||||
from irctest.numerics import RPL_LIST, RPL_LISTEND, RPL_LISTSTART
|
|
||||||
|
|
||||||
|
|
||||||
class _BasedListTestCase(cases.BaseServerTestCase):
|
class ListTestCase(cases.BaseServerTestCase):
|
||||||
def _parseChanList(self, client):
|
|
||||||
channels = set()
|
|
||||||
while True:
|
|
||||||
m = self.getMessage(client)
|
|
||||||
if m.command == RPL_LISTEND:
|
|
||||||
break
|
|
||||||
if m.command == RPL_LIST:
|
|
||||||
if m.params[1].startswith("&"):
|
|
||||||
# skip local pseudo-channels listed by ngircd and ircu
|
|
||||||
continue
|
|
||||||
channels.add(m.params[1])
|
|
||||||
|
|
||||||
return channels
|
|
||||||
|
|
||||||
|
|
||||||
class ListTestCase(_BasedListTestCase):
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812")
|
@cases.mark_specifications("RFC1459", "RFC2812")
|
||||||
@cases.xfailIfSoftware(["irc2"], "irc2 deprecated LIST")
|
|
||||||
def testListEmpty(self):
|
def testListEmpty(self):
|
||||||
"""<https://tools.ietf.org/html/rfc1459#section-4.2.6>
|
"""<https://tools.ietf.org/html/rfc1459#section-4.2.6>
|
||||||
<https://tools.ietf.org/html/rfc2812#section-3.2.6>
|
<https://tools.ietf.org/html/rfc2812#section-3.2.6>
|
||||||
<https://modern.ircdocs.horse/#list-message>
|
|
||||||
"""
|
"""
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
self.sendLine(2, "LIST")
|
self.sendLine(2, "LIST")
|
||||||
m = self.getMessage(2)
|
m = self.getMessage(2)
|
||||||
if m.command == RPL_LISTSTART:
|
if m.command == "321":
|
||||||
# skip
|
# skip RPL_LISTSTART
|
||||||
m = self.getMessage(2)
|
m = self.getMessage(2)
|
||||||
# skip local pseudo-channels listed by ngircd and ircu
|
while m.command == "322" and m.params[1] == "&SERVER":
|
||||||
while m.command == RPL_LIST and m.params[1].startswith("&"):
|
# ngircd adds this pseudo-channel
|
||||||
m = self.getMessage(2)
|
m = self.getMessage(2)
|
||||||
self.assertNotEqual(
|
self.assertNotEqual(
|
||||||
m.command,
|
m.command,
|
||||||
RPL_LIST,
|
"322", # RPL_LIST
|
||||||
"LIST response gives (at least) one channel, whereas there " "is none.",
|
"LIST response gives (at least) one channel, whereas there " "is none.",
|
||||||
)
|
)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
command=RPL_LISTEND,
|
command="323", # RPL_LISTEND
|
||||||
fail_msg="Second reply to LIST is not 322 (RPL_LIST) "
|
fail_msg="Second reply to LIST is not 322 (RPL_LIST) "
|
||||||
"or 323 (RPL_LISTEND), or but: {msg}",
|
"or 323 (RPL_LISTEND), or but: {msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812")
|
@cases.mark_specifications("RFC1459", "RFC2812")
|
||||||
@cases.xfailIfSoftware(["irc2"], "irc2 deprecated LIST")
|
|
||||||
def testListOne(self):
|
def testListOne(self):
|
||||||
"""When a channel exists, LIST should get it in a reply.
|
"""When a channel exists, LIST should get it in a reply.
|
||||||
<https://tools.ietf.org/html/rfc1459#section-4.2.6>
|
<https://tools.ietf.org/html/rfc1459#section-4.2.6>
|
||||||
<https://tools.ietf.org/html/rfc2812#section-3.2.6>
|
<https://tools.ietf.org/html/rfc2812#section-3.2.6>
|
||||||
|
|
||||||
<https://modern.ircdocs.horse/#list-message>
|
|
||||||
"""
|
"""
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
@ -73,331 +42,34 @@ class ListTestCase(_BasedListTestCase):
|
|||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
self.sendLine(2, "LIST")
|
self.sendLine(2, "LIST")
|
||||||
m = self.getMessage(2)
|
m = self.getMessage(2)
|
||||||
if m.command == RPL_LISTSTART:
|
if m.command == "321":
|
||||||
# skip
|
# skip RPL_LISTSTART
|
||||||
m = self.getMessage(2)
|
m = self.getMessage(2)
|
||||||
self.assertNotEqual(
|
self.assertNotEqual(
|
||||||
m.command,
|
m.command,
|
||||||
RPL_LISTEND,
|
"323", # RPL_LISTEND
|
||||||
fail_msg="LIST response ended (ie. 323, aka RPL_LISTEND) "
|
fail_msg="LIST response ended (ie. 323, aka RPL_LISTEND) "
|
||||||
"without listing any channel, whereas there is one.",
|
"without listing any channel, whereas there is one.",
|
||||||
)
|
)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
command=RPL_LIST,
|
command="322", # RPL_LIST
|
||||||
fail_msg="Second reply to LIST is not 322 (RPL_LIST), "
|
fail_msg="Second reply to LIST is not 322 (RPL_LIST), "
|
||||||
"nor 323 (RPL_LISTEND) but: {msg}",
|
"nor 323 (RPL_LISTEND) but: {msg}",
|
||||||
)
|
)
|
||||||
m = self.getMessage(2)
|
m = self.getMessage(2)
|
||||||
# skip local pseudo-channels listed by ngircd and ircu
|
while m.command == "322" and m.params[1] == "&SERVER":
|
||||||
while m.command == RPL_LIST and m.params[1].startswith("&"):
|
# ngircd adds this pseudo-channel
|
||||||
m = self.getMessage(2)
|
m = self.getMessage(2)
|
||||||
self.assertNotEqual(
|
self.assertNotEqual(
|
||||||
m.command,
|
m.command,
|
||||||
RPL_LIST,
|
"322", # RPL_LIST
|
||||||
fail_msg="LIST response gives (at least) two channels, "
|
fail_msg="LIST response gives (at least) two channels, "
|
||||||
"whereas there is only one.",
|
"whereas there is only one.",
|
||||||
)
|
)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
command=RPL_LISTEND,
|
command="323", # RPL_LISTEND
|
||||||
fail_msg="Third reply to LIST is not 322 (RPL_LIST) "
|
fail_msg="Third reply to LIST is not 322 (RPL_LIST) "
|
||||||
"or 323 (RPL_LISTEND), or but: {msg}",
|
"or 323 (RPL_LISTEND), or but: {msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812")
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["Charybdis", "Solanum"],
|
|
||||||
"Charybdis and Solanum insert ERR_NOSUCHNICK reply in LIST",
|
|
||||||
)
|
|
||||||
def testListNonexistent(self):
|
|
||||||
"""LIST on a nonexistent channel does not send an error
|
|
||||||
response.
|
|
||||||
<https://tools.ietf.org/html/rfc1459#section-4.2.6>
|
|
||||||
<https://tools.ietf.org/html/rfc2812#section-3.2.6>
|
|
||||||
"""
|
|
||||||
self.connectClient("bar")
|
|
||||||
self.sendLine(1, "LIST #nonexistent")
|
|
||||||
responses = {msg.command for msg in self.getMessages(1)}
|
|
||||||
# successful response MUST include RPL_LISTEND:
|
|
||||||
self.assertIn(RPL_LISTEND, responses)
|
|
||||||
# and MUST NOT include RPL_LIST (since there is no matching channel)
|
|
||||||
# or any error numerics:
|
|
||||||
self.assertLessEqual(responses, {RPL_LISTSTART, RPL_LISTEND})
|
|
||||||
|
|
||||||
@cases.mark_isupport("ELIST")
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testListMask(self):
|
|
||||||
"""
|
|
||||||
"M: Searching based on mask."
|
|
||||||
-- <https://modern.ircdocs.horse/#elist-parameter>
|
|
||||||
-- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8
|
|
||||||
"""
|
|
||||||
self.connectClient("foo")
|
|
||||||
|
|
||||||
if "M" not in self.server_support.get("ELIST", ""):
|
|
||||||
raise runner.OptionalExtensionNotSupported("ELIST=M")
|
|
||||||
|
|
||||||
self.connectClient("bar")
|
|
||||||
self.sendLine(1, "JOIN #chan1")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.sendLine(1, "JOIN #chan2")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST *an1")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan1"})
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST *an2")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan2"})
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST #c*n2")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan2"})
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST *an3")
|
|
||||||
self.assertEqual(self._parseChanList(2), set())
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST #ch*")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"})
|
|
||||||
|
|
||||||
@cases.mark_isupport("ELIST")
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testListNotMask(self):
|
|
||||||
"""
|
|
||||||
" N: Searching based on a non-matching mask. i.e., the opposite of M."
|
|
||||||
-- <https://modern.ircdocs.horse/#elist-parameter>
|
|
||||||
-- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8
|
|
||||||
"""
|
|
||||||
self.connectClient("foo")
|
|
||||||
|
|
||||||
if "N" not in self.server_support.get("ELIST", ""):
|
|
||||||
raise runner.OptionalExtensionNotSupported("ELIST=N")
|
|
||||||
|
|
||||||
self.sendLine(1, "JOIN #chan1")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.sendLine(1, "JOIN #chan2")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.connectClient("bar")
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST !*an1")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan2"})
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST !*an2")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan1"})
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST !#c*n2")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan1"})
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST !*an3")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"})
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST !#ch*")
|
|
||||||
self.assertEqual(self._parseChanList(2), set())
|
|
||||||
|
|
||||||
@cases.mark_isupport("ELIST")
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
def testListUsers(self):
|
|
||||||
"""
|
|
||||||
"U: Searching based on user count within the channel, via the "<val" and
|
|
||||||
">val" modifiers to search for a channel that has less or more than val users,
|
|
||||||
respectively."
|
|
||||||
-- <https://modern.ircdocs.horse/#elist-parameter>
|
|
||||||
-- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8
|
|
||||||
"""
|
|
||||||
self.connectClient("foo")
|
|
||||||
|
|
||||||
if "U" not in self.server_support.get("ELIST", ""):
|
|
||||||
raise runner.OptionalExtensionNotSupported("ELIST=U")
|
|
||||||
|
|
||||||
self.sendLine(1, "JOIN #chan1")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.sendLine(1, "JOIN #chan2")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.connectClient("bar")
|
|
||||||
self.sendLine(2, "JOIN #chan2")
|
|
||||||
self.getMessages(2)
|
|
||||||
|
|
||||||
self.connectClient("baz")
|
|
||||||
|
|
||||||
self.sendLine(3, "LIST >0")
|
|
||||||
self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"})
|
|
||||||
|
|
||||||
self.sendLine(3, "LIST <1")
|
|
||||||
self.assertEqual(self._parseChanList(3), set())
|
|
||||||
|
|
||||||
self.sendLine(3, "LIST <100")
|
|
||||||
self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"})
|
|
||||||
|
|
||||||
self.sendLine(3, "LIST >1")
|
|
||||||
self.assertEqual(self._parseChanList(3), {"#chan2"})
|
|
||||||
|
|
||||||
self.sendLine(3, "LIST <2")
|
|
||||||
self.assertEqual(self._parseChanList(3), {"#chan1"})
|
|
||||||
|
|
||||||
self.sendLine(3, "LIST <100")
|
|
||||||
self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"})
|
|
||||||
|
|
||||||
|
|
||||||
class FaketimeListTestCase(_BasedListTestCase):
|
|
||||||
faketime = "+1y x30" # for every wall clock second, 1 minute passed for the server
|
|
||||||
|
|
||||||
def _sleep_minutes(self, n):
|
|
||||||
for _ in range(n):
|
|
||||||
if self.controller.faketime_enabled:
|
|
||||||
# From the server's point of view, 1 min will pass
|
|
||||||
time.sleep(2)
|
|
||||||
else:
|
|
||||||
time.sleep(60)
|
|
||||||
|
|
||||||
# reply to pings
|
|
||||||
self.getMessages(1)
|
|
||||||
self.getMessages(2)
|
|
||||||
|
|
||||||
@cases.mark_isupport("ELIST")
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["Plexus4", "Hybrid"],
|
|
||||||
"Hybrid and Plexus4 filter on ELIST=C with the opposite meaning",
|
|
||||||
)
|
|
||||||
@cases.xfailIf(
|
|
||||||
lambda self: bool(
|
|
||||||
self.controller.software_name == "UnrealIRCd"
|
|
||||||
and self.controller.software_version == 5
|
|
||||||
),
|
|
||||||
"UnrealIRCd <6.0.3 filters on ELIST=C with the opposite meaning",
|
|
||||||
)
|
|
||||||
def testListCreationTime(self):
|
|
||||||
"""
|
|
||||||
" C: Searching based on channel creation time, via the "C<val" and "C>val"
|
|
||||||
modifiers to search for a channel creation time that is higher or lower
|
|
||||||
than val."
|
|
||||||
-- <https://modern.ircdocs.horse/#elist-parameter>
|
|
||||||
-- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8
|
|
||||||
|
|
||||||
Unfortunately, this is ambiguous, because "val" is a time delta (in minutes),
|
|
||||||
not a timestamp.
|
|
||||||
|
|
||||||
On InspIRCd and Charybdis/Solanum, "C<val" is interpreted as "the channel was
|
|
||||||
created less than <val> minutes ago
|
|
||||||
|
|
||||||
On UnrealIRCd, Plexus, and Hybrid, it is interpreted as "the channel's creation
|
|
||||||
time is a timestamp lower than <val> minutes ago" (ie. the exact opposite)
|
|
||||||
|
|
||||||
"C: Searching based on channel creation time, via the "C<val" and "C>val"
|
|
||||||
modifiers to search for a channel that was created either less than `val`
|
|
||||||
minutes ago, or more than `val` minutes ago, respectively"
|
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/171
|
|
||||||
"""
|
|
||||||
self.connectClient("foo")
|
|
||||||
|
|
||||||
if "C" not in self.server_support.get("ELIST", ""):
|
|
||||||
raise runner.OptionalExtensionNotSupported("ELIST=C")
|
|
||||||
|
|
||||||
self.connectClient("bar")
|
|
||||||
self.sendLine(1, "JOIN #chan1")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
# Helps debugging
|
|
||||||
self.sendLine(1, "TIME")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self._sleep_minutes(2)
|
|
||||||
|
|
||||||
# Helps debugging
|
|
||||||
self.sendLine(1, "TIME")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "JOIN #chan2")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self._sleep_minutes(1)
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST C>2")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan1"})
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST C<2")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan2"})
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST C<0")
|
|
||||||
if self.controller.software_name == "InspIRCd":
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"})
|
|
||||||
else:
|
|
||||||
self.assertEqual(self._parseChanList(2), set())
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST C>0")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"})
|
|
||||||
|
|
||||||
self.sendLine(2, "LIST C<10")
|
|
||||||
self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"})
|
|
||||||
|
|
||||||
@cases.mark_isupport("ELIST")
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@cases.xfailIf(
|
|
||||||
lambda self: bool(
|
|
||||||
self.controller.software_name == "UnrealIRCd"
|
|
||||||
and self.controller.software_version == 5
|
|
||||||
),
|
|
||||||
"UnrealIRCd <6.0.3 advertises ELIST=T but does not implement it",
|
|
||||||
)
|
|
||||||
def testListTopicTime(self):
|
|
||||||
"""
|
|
||||||
"T: Searching based on topic time, via the "T<val" and "T>val"
|
|
||||||
modifiers to search for a topic time that is lower or higher than
|
|
||||||
val respectively."
|
|
||||||
-- <https://modern.ircdocs.horse/#elist-parameter>
|
|
||||||
-- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8
|
|
||||||
|
|
||||||
See testListCreationTime's docstring for comments on this.
|
|
||||||
|
|
||||||
"T: Searching based on topic set time, via the "T<val" and "T>val" modifiers
|
|
||||||
to search for a topic time that was set less than `val` minutes ago, or more
|
|
||||||
than `val` minutes ago, respectively."
|
|
||||||
-- https://github.com/ircdocs/modern-irc/pull/171
|
|
||||||
"""
|
|
||||||
self.connectClient("foo")
|
|
||||||
|
|
||||||
if "T" not in self.server_support.get("ELIST", ""):
|
|
||||||
raise runner.OptionalExtensionNotSupported("ELIST=T")
|
|
||||||
|
|
||||||
self.connectClient("bar")
|
|
||||||
self.sendLine(1, "JOIN #chan1")
|
|
||||||
self.sendLine(1, "JOIN #chan2")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "TOPIC #chan1 :First channel")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
# Helps debugging
|
|
||||||
self.sendLine(1, "TIME")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self._sleep_minutes(2)
|
|
||||||
|
|
||||||
# Helps debugging
|
|
||||||
self.sendLine(1, "TIME")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "TOPIC #chan2 :Second channel")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self._sleep_minutes(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "LIST T>2")
|
|
||||||
self.assertEqual(self._parseChanList(1), {"#chan1"})
|
|
||||||
|
|
||||||
self.sendLine(1, "LIST T<2")
|
|
||||||
self.assertEqual(self._parseChanList(1), {"#chan2"})
|
|
||||||
|
|
||||||
self.sendLine(1, "LIST T<0")
|
|
||||||
if self.controller.software_name == "InspIRCd":
|
|
||||||
# Insp internally represents "LIST T>0" like "LIST"
|
|
||||||
self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"})
|
|
||||||
else:
|
|
||||||
self.assertEqual(self._parseChanList(1), set())
|
|
||||||
|
|
||||||
self.sendLine(1, "LIST T>0")
|
|
||||||
self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"})
|
|
||||||
|
|
||||||
self.sendLine(1, "LIST T<10")
|
|
||||||
self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"})
|
|
||||||
|
@ -1,11 +1,3 @@
|
|||||||
"""
|
|
||||||
The LUSERS command (`RFC 2812
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.2>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#lusers-message>`__),
|
|
||||||
which provides statistics on user counts.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import re
|
import re
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
@ -22,7 +14,6 @@ from irctest.numerics import (
|
|||||||
RPL_LUSERUNKNOWN,
|
RPL_LUSERUNKNOWN,
|
||||||
RPL_YOUREOPER,
|
RPL_YOUREOPER,
|
||||||
)
|
)
|
||||||
from irctest.patma import ANYSTR, StrRe
|
|
||||||
|
|
||||||
# 3 numbers, delimited by spaces, possibly negative (eek)
|
# 3 numbers, delimited by spaces, possibly negative (eek)
|
||||||
LUSERCLIENT_REGEX = re.compile(r"^.*( [-0-9]* ).*( [-0-9]* ).*( [-0-9]* ).*$")
|
LUSERCLIENT_REGEX = re.compile(r"^.*( [-0-9]* ).*( [-0-9]* ).*( [-0-9]* ).*$")
|
||||||
@ -59,11 +50,10 @@ class LusersTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertIn(lusers.LocalTotal, (total, None))
|
self.assertIn(lusers.LocalTotal, (total, None))
|
||||||
self.assertIn(lusers.LocalMax, (max_, None))
|
self.assertIn(lusers.LocalMax, (max_, None))
|
||||||
|
|
||||||
def getLusers(self, client, allow_missing_265_266):
|
def getLusers(self, client):
|
||||||
self.sendLine(client, "LUSERS")
|
self.sendLine(client, "LUSERS")
|
||||||
messages = self.getMessages(client)
|
messages = self.getMessages(client)
|
||||||
by_numeric = dict((msg.command, msg) for msg in messages)
|
by_numeric = dict((msg.command, msg) for msg in messages)
|
||||||
self.assertEqual(len(by_numeric), len(messages), "Duplicated numerics")
|
|
||||||
|
|
||||||
result = LusersResult()
|
result = LusersResult()
|
||||||
|
|
||||||
@ -83,31 +73,12 @@ class LusersTestCase(cases.BaseServerTestCase):
|
|||||||
raise ValueError("corrupt reply for 251 RPL_LUSERCLIENT", luserclient_param)
|
raise ValueError("corrupt reply for 251 RPL_LUSERCLIENT", luserclient_param)
|
||||||
|
|
||||||
if RPL_LUSEROP in by_numeric:
|
if RPL_LUSEROP in by_numeric:
|
||||||
self.assertMessageMatch(
|
|
||||||
by_numeric[RPL_LUSEROP], params=[client, StrRe("[0-9]+"), ANYSTR]
|
|
||||||
)
|
|
||||||
result.Opers = int(by_numeric[RPL_LUSEROP].params[1])
|
result.Opers = int(by_numeric[RPL_LUSEROP].params[1])
|
||||||
if RPL_LUSERUNKNOWN in by_numeric:
|
if RPL_LUSERUNKNOWN in by_numeric:
|
||||||
self.assertMessageMatch(
|
|
||||||
by_numeric[RPL_LUSERUNKNOWN], params=[client, StrRe("[0-9]+"), ANYSTR]
|
|
||||||
)
|
|
||||||
result.Unregistered = int(by_numeric[RPL_LUSERUNKNOWN].params[1])
|
result.Unregistered = int(by_numeric[RPL_LUSERUNKNOWN].params[1])
|
||||||
if RPL_LUSERCHANNELS in by_numeric:
|
if RPL_LUSERCHANNELS in by_numeric:
|
||||||
self.assertMessageMatch(
|
|
||||||
by_numeric[RPL_LUSERCHANNELS], params=[client, StrRe("[0-9]+"), ANYSTR]
|
|
||||||
)
|
|
||||||
result.Channels = int(by_numeric[RPL_LUSERCHANNELS].params[1])
|
result.Channels = int(by_numeric[RPL_LUSERCHANNELS].params[1])
|
||||||
|
|
||||||
self.assertMessageMatch(by_numeric[RPL_LUSERCLIENT], params=[client, ANYSTR])
|
|
||||||
self.assertMessageMatch(by_numeric[RPL_LUSERME], params=[client, ANYSTR])
|
|
||||||
|
|
||||||
if (
|
|
||||||
allow_missing_265_266
|
|
||||||
and RPL_LOCALUSERS not in by_numeric
|
|
||||||
and RPL_GLOBALUSERS not in by_numeric
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
# FIXME: RPL_LOCALUSERS and RPL_GLOBALUSERS are only in Modern, not in RFC2812
|
# FIXME: RPL_LOCALUSERS and RPL_GLOBALUSERS are only in Modern, not in RFC2812
|
||||||
localusers = by_numeric[RPL_LOCALUSERS]
|
localusers = by_numeric[RPL_LOCALUSERS]
|
||||||
globalusers = by_numeric[RPL_GLOBALUSERS]
|
globalusers = by_numeric[RPL_GLOBALUSERS]
|
||||||
@ -143,55 +114,23 @@ class BasicLusersTestCase(LusersTestCase):
|
|||||||
@cases.mark_specifications("RFC2812")
|
@cases.mark_specifications("RFC2812")
|
||||||
def testLusers(self):
|
def testLusers(self):
|
||||||
self.connectClient("bar", name="bar")
|
self.connectClient("bar", name="bar")
|
||||||
self.getLusers("bar", True)
|
lusers = self.getLusers("bar")
|
||||||
|
|
||||||
self.connectClient("qux", name="qux")
|
|
||||||
self.getLusers("qux", True)
|
|
||||||
|
|
||||||
self.sendLine("qux", "QUIT")
|
|
||||||
self.assertDisconnected("qux")
|
|
||||||
self.getLusers("bar", True)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["ircu2", "Nefarious", "snircd"],
|
|
||||||
"test depends on Modern behavior, not just RFC2812",
|
|
||||||
)
|
|
||||||
def testLusersFull(self):
|
|
||||||
self.connectClient("bar", name="bar")
|
|
||||||
lusers = self.getLusers("bar", False)
|
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=1, max_=1)
|
self.assertLusersResult(lusers, unregistered=0, total=1, max_=1)
|
||||||
|
|
||||||
self.connectClient("qux", name="qux")
|
self.connectClient("qux", name="qux")
|
||||||
lusers = self.getLusers("qux", False)
|
lusers = self.getLusers("qux")
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
||||||
|
|
||||||
self.sendLine("qux", "QUIT")
|
self.sendLine("qux", "QUIT")
|
||||||
self.assertDisconnected("qux")
|
self.assertDisconnected("qux")
|
||||||
lusers = self.getLusers("bar", False)
|
lusers = self.getLusers("bar")
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=1, max_=2)
|
self.assertLusersResult(lusers, unregistered=0, total=1, max_=2)
|
||||||
|
|
||||||
|
|
||||||
class LusersUnregisteredTestCase(LusersTestCase):
|
class LusersUnregisteredTestCase(LusersTestCase):
|
||||||
@cases.mark_specifications("RFC2812")
|
@cases.mark_specifications("RFC2812")
|
||||||
@cases.xfailIfSoftware(
|
def testLusers(self):
|
||||||
["Nefarious"],
|
self.doLusersTest()
|
||||||
"Nefarious doesn't seem to distinguish unregistered users from normal ones",
|
|
||||||
)
|
|
||||||
def testLusersRfc2812(self):
|
|
||||||
self.doLusersTest(True)
|
|
||||||
|
|
||||||
@cases.mark_specifications("Modern")
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["Nefarious"],
|
|
||||||
"Nefarious doesn't seem to distinguish unregistered users from normal ones",
|
|
||||||
)
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["ircu2", "Nefarious", "snircd"],
|
|
||||||
"test depends on Modern behavior, not just RFC2812",
|
|
||||||
)
|
|
||||||
def testLusersFull(self):
|
|
||||||
self.doLusersTest(False)
|
|
||||||
|
|
||||||
def _synchronize(self, client_name):
|
def _synchronize(self, client_name):
|
||||||
"""Synchronizes using a PING, but accept ERR_NOTREGISTERED as a response."""
|
"""Synchronizes using a PING, but accept ERR_NOTREGISTERED as a response."""
|
||||||
@ -206,39 +145,34 @@ class LusersUnregisteredTestCase(LusersTestCase):
|
|||||||
"got neither PONG or ERR_NOTREGISTERED"
|
"got neither PONG or ERR_NOTREGISTERED"
|
||||||
)
|
)
|
||||||
|
|
||||||
def doLusersTest(self, allow_missing_265_266):
|
def doLusersTest(self):
|
||||||
self.connectClient("bar", name="bar")
|
self.connectClient("bar", name="bar")
|
||||||
lusers = self.getLusers("bar", allow_missing_265_266)
|
lusers = self.getLusers("bar")
|
||||||
if lusers:
|
self.assertLusersResult(lusers, unregistered=0, total=1, max_=1)
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=1, max_=1)
|
|
||||||
|
|
||||||
self.addClient("qux")
|
self.addClient("qux")
|
||||||
self.sendLine("qux", "NICK qux")
|
self.sendLine("qux", "NICK qux")
|
||||||
self._synchronize("qux")
|
self._synchronize("qux")
|
||||||
lusers = self.getLusers("bar", allow_missing_265_266)
|
lusers = self.getLusers("bar")
|
||||||
if lusers:
|
self.assertLusersResult(lusers, unregistered=1, total=1, max_=1)
|
||||||
self.assertLusersResult(lusers, unregistered=1, total=1, max_=1)
|
|
||||||
|
|
||||||
self.addClient("bat")
|
self.addClient("bat")
|
||||||
self.sendLine("bat", "NICK bat")
|
self.sendLine("bat", "NICK bat")
|
||||||
self._synchronize("bat")
|
self._synchronize("bat")
|
||||||
lusers = self.getLusers("bar", allow_missing_265_266)
|
lusers = self.getLusers("bar")
|
||||||
if lusers:
|
self.assertLusersResult(lusers, unregistered=2, total=1, max_=1)
|
||||||
self.assertLusersResult(lusers, unregistered=2, total=1, max_=1)
|
|
||||||
|
|
||||||
# complete registration on one client
|
# complete registration on one client
|
||||||
self.sendLine("qux", "USER u s e r")
|
self.sendLine("qux", "USER u s e r")
|
||||||
self.getRegistrationMessage("qux")
|
self.getRegistrationMessage("qux")
|
||||||
lusers = self.getLusers("bar", allow_missing_265_266)
|
lusers = self.getLusers("bar")
|
||||||
if lusers:
|
self.assertLusersResult(lusers, unregistered=1, total=2, max_=2)
|
||||||
self.assertLusersResult(lusers, unregistered=1, total=2, max_=2)
|
|
||||||
|
|
||||||
# QUIT the other without registering
|
# QUIT the other without registering
|
||||||
self.sendLine("bat", "QUIT")
|
self.sendLine("bat", "QUIT")
|
||||||
self.assertDisconnected("bat")
|
self.assertDisconnected("bat")
|
||||||
lusers = self.getLusers("bar", allow_missing_265_266)
|
lusers = self.getLusers("bar")
|
||||||
if lusers:
|
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
|
||||||
|
|
||||||
|
|
||||||
class LusersUnregisteredDefaultInvisibleTestCase(LusersUnregisteredTestCase):
|
class LusersUnregisteredDefaultInvisibleTestCase(LusersUnregisteredTestCase):
|
||||||
@ -253,13 +187,9 @@ class LusersUnregisteredDefaultInvisibleTestCase(LusersUnregisteredTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("Ergo")
|
@cases.mark_specifications("Ergo")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["Nefarious"],
|
|
||||||
"Nefarious doesn't seem to distinguish unregistered users from normal ones",
|
|
||||||
)
|
|
||||||
def testLusers(self):
|
def testLusers(self):
|
||||||
self.doLusersTest(False)
|
self.doLusersTest()
|
||||||
lusers = self.getLusers("bar", False)
|
lusers = self.getLusers("bar")
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
||||||
self.assertEqual(lusers.GlobalInvisible, 2)
|
self.assertEqual(lusers.GlobalInvisible, 2)
|
||||||
self.assertEqual(lusers.GlobalVisible, 0)
|
self.assertEqual(lusers.GlobalVisible, 0)
|
||||||
@ -269,7 +199,7 @@ class LuserOpersTestCase(LusersTestCase):
|
|||||||
@cases.mark_specifications("Ergo")
|
@cases.mark_specifications("Ergo")
|
||||||
def testLuserOpers(self):
|
def testLuserOpers(self):
|
||||||
self.connectClient("bar", name="bar")
|
self.connectClient("bar", name="bar")
|
||||||
lusers = self.getLusers("bar", False)
|
lusers = self.getLusers("bar")
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=1, max_=1)
|
self.assertLusersResult(lusers, unregistered=0, total=1, max_=1)
|
||||||
self.assertIn(lusers.Opers, (0, None))
|
self.assertIn(lusers.Opers, (0, None))
|
||||||
|
|
||||||
@ -277,7 +207,7 @@ class LuserOpersTestCase(LusersTestCase):
|
|||||||
self.sendLine("bar", "OPER operuser operpassword")
|
self.sendLine("bar", "OPER operuser operpassword")
|
||||||
msgs = self.getMessages("bar")
|
msgs = self.getMessages("bar")
|
||||||
self.assertIn(RPL_YOUREOPER, {msg.command for msg in msgs})
|
self.assertIn(RPL_YOUREOPER, {msg.command for msg in msgs})
|
||||||
lusers = self.getLusers("bar", False)
|
lusers = self.getLusers("bar")
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=1, max_=1)
|
self.assertLusersResult(lusers, unregistered=0, total=1, max_=1)
|
||||||
self.assertEqual(lusers.Opers, 1)
|
self.assertEqual(lusers.Opers, 1)
|
||||||
|
|
||||||
@ -285,7 +215,7 @@ class LuserOpersTestCase(LusersTestCase):
|
|||||||
self.connectClient("qux", name="qux")
|
self.connectClient("qux", name="qux")
|
||||||
self.sendLine("qux", "OPER operuser operpassword")
|
self.sendLine("qux", "OPER operuser operpassword")
|
||||||
self.getMessages("qux")
|
self.getMessages("qux")
|
||||||
lusers = self.getLusers("bar", False)
|
lusers = self.getLusers("bar")
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
||||||
self.assertEqual(lusers.Opers, 2)
|
self.assertEqual(lusers.Opers, 2)
|
||||||
|
|
||||||
@ -293,14 +223,14 @@ class LuserOpersTestCase(LusersTestCase):
|
|||||||
self.sendLine("bar", "MODE bar -o")
|
self.sendLine("bar", "MODE bar -o")
|
||||||
msgs = self.getMessages("bar")
|
msgs = self.getMessages("bar")
|
||||||
self.assertIn("MODE", {msg.command for msg in msgs})
|
self.assertIn("MODE", {msg.command for msg in msgs})
|
||||||
lusers = self.getLusers("bar", False)
|
lusers = self.getLusers("bar")
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
||||||
self.assertEqual(lusers.Opers, 1)
|
self.assertEqual(lusers.Opers, 1)
|
||||||
|
|
||||||
# remove oper by quit
|
# remove oper by quit
|
||||||
self.sendLine("qux", "QUIT")
|
self.sendLine("qux", "QUIT")
|
||||||
self.assertDisconnected("qux")
|
self.assertDisconnected("qux")
|
||||||
lusers = self.getLusers("bar", False)
|
lusers = self.getLusers("bar")
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=1, max_=2)
|
self.assertLusersResult(lusers, unregistered=0, total=1, max_=2)
|
||||||
self.assertEqual(lusers.Opers, 0)
|
self.assertEqual(lusers.Opers, 0)
|
||||||
|
|
||||||
@ -317,13 +247,13 @@ class ErgoInvisibleDefaultTestCase(LusersTestCase):
|
|||||||
@cases.mark_specifications("Ergo")
|
@cases.mark_specifications("Ergo")
|
||||||
def testLusers(self):
|
def testLusers(self):
|
||||||
self.connectClient("bar", name="bar")
|
self.connectClient("bar", name="bar")
|
||||||
lusers = self.getLusers("bar", False)
|
lusers = self.getLusers("bar")
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=1, max_=1)
|
self.assertLusersResult(lusers, unregistered=0, total=1, max_=1)
|
||||||
self.assertEqual(lusers.GlobalInvisible, 1)
|
self.assertEqual(lusers.GlobalInvisible, 1)
|
||||||
self.assertEqual(lusers.GlobalVisible, 0)
|
self.assertEqual(lusers.GlobalVisible, 0)
|
||||||
|
|
||||||
self.connectClient("qux", name="qux")
|
self.connectClient("qux", name="qux")
|
||||||
lusers = self.getLusers("qux", False)
|
lusers = self.getLusers("qux")
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
||||||
self.assertEqual(lusers.GlobalInvisible, 2)
|
self.assertEqual(lusers.GlobalInvisible, 2)
|
||||||
self.assertEqual(lusers.GlobalVisible, 0)
|
self.assertEqual(lusers.GlobalVisible, 0)
|
||||||
@ -331,7 +261,7 @@ class ErgoInvisibleDefaultTestCase(LusersTestCase):
|
|||||||
# remove +i with MODE
|
# remove +i with MODE
|
||||||
self.sendLine("bar", "MODE bar -i")
|
self.sendLine("bar", "MODE bar -i")
|
||||||
msgs = self.getMessages("bar")
|
msgs = self.getMessages("bar")
|
||||||
lusers = self.getLusers("bar", False)
|
lusers = self.getLusers("bar")
|
||||||
self.assertIn("MODE", {msg.command for msg in msgs})
|
self.assertIn("MODE", {msg.command for msg in msgs})
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
self.assertLusersResult(lusers, unregistered=0, total=2, max_=2)
|
||||||
self.assertEqual(lusers.GlobalInvisible, 1)
|
self.assertEqual(lusers.GlobalInvisible, 1)
|
||||||
@ -340,7 +270,7 @@ class ErgoInvisibleDefaultTestCase(LusersTestCase):
|
|||||||
# disconnect invisible user
|
# disconnect invisible user
|
||||||
self.sendLine("qux", "QUIT")
|
self.sendLine("qux", "QUIT")
|
||||||
self.assertDisconnected("qux")
|
self.assertDisconnected("qux")
|
||||||
lusers = self.getLusers("bar", False)
|
lusers = self.getLusers("bar")
|
||||||
self.assertLusersResult(lusers, unregistered=0, total=1, max_=2)
|
self.assertLusersResult(lusers, unregistered=0, total=1, max_=2)
|
||||||
self.assertEqual(lusers.GlobalInvisible, 0)
|
self.assertEqual(lusers.GlobalInvisible, 0)
|
||||||
self.assertEqual(lusers.GlobalVisible, 1)
|
self.assertEqual(lusers.GlobalVisible, 1)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
`IRCv3 message-tags <https://ircv3.net/specs/extensions/message-tags>`_
|
https://ircv3.net/specs/extensions/message-tags.html
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -10,7 +10,7 @@ from irctest.numerics import ERR_INPUTTOOLONG
|
|||||||
from irctest.patma import ANYDICT, ANYSTR, StrRe
|
from irctest.patma import ANYDICT, ANYSTR, StrRe
|
||||||
|
|
||||||
|
|
||||||
class MessageTagsTestCase(cases.BaseServerTestCase):
|
class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||||
@pytest.mark.arbitrary_client_tags
|
@pytest.mark.arbitrary_client_tags
|
||||||
@cases.mark_capabilities("message-tags")
|
@cases.mark_capabilities("message-tags")
|
||||||
def testBasic(self):
|
def testBasic(self):
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
The PRIVMSG and NOTICE commands.
|
Section 3.2 of RFC 2812
|
||||||
|
<https://tools.ietf.org/html/rfc2812#section-3.3>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases, runner
|
||||||
from irctest.numerics import ERR_INPUTTOOLONG
|
from irctest.numerics import ERR_INPUTTOOLONG, ERR_NOPRIVILEGES, ERR_NOSUCHNICK
|
||||||
from irctest.patma import ANYSTR
|
|
||||||
|
|
||||||
|
|
||||||
class PrivmsgTestCase(cases.BaseServerTestCase):
|
class PrivmsgTestCase(cases.BaseServerTestCase):
|
||||||
@ -33,25 +33,96 @@ class PrivmsgTestCase(cases.BaseServerTestCase):
|
|||||||
# ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL, or ERR_CANNOTSENDTOCHAN
|
# ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL, or ERR_CANNOTSENDTOCHAN
|
||||||
self.assertIn(msg.command, ("401", "403", "404"))
|
self.assertIn(msg.command, ("401", "403", "404"))
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812")
|
|
||||||
def testPrivmsgToUser(self):
|
|
||||||
"""<https://tools.ietf.org/html/rfc2812#section-3.3.1>"""
|
|
||||||
self.connectClient("foo")
|
|
||||||
self.connectClient("bar")
|
|
||||||
self.sendLine(1, "PRIVMSG bar :hey there!")
|
|
||||||
self.getMessages(1)
|
|
||||||
pms = [msg for msg in self.getMessages(2) if msg.command == "PRIVMSG"]
|
|
||||||
self.assertEqual(len(pms), 1)
|
|
||||||
self.assertMessageMatch(pms[0], command="PRIVMSG", params=["bar", "hey there!"])
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812")
|
class PrivmsgServermaskTestCase(cases.BaseServerTestCase):
|
||||||
def testPrivmsgNonexistentUser(self):
|
def setUp(self):
|
||||||
"""<https://tools.ietf.org/html/rfc2812#section-3.3.1>"""
|
super().setUp()
|
||||||
self.connectClient("foo")
|
|
||||||
self.sendLine(1, "PRIVMSG bar :hey there!")
|
self.connectClient("chk", "chk")
|
||||||
msg = self.getMessage(1)
|
self.sendLine("chk", "PRIVMSG $my.little.server :hello there")
|
||||||
# ERR_NOSUCHNICK: 401 <sender> <recipient> :No such nick
|
msg = self.getMessage("chk")
|
||||||
self.assertMessageMatch(msg, command="401", params=["foo", "bar", ANYSTR])
|
if msg.command == ERR_NOSUCHNICK:
|
||||||
|
raise runner.NotImplementedByController("PRIVMSG to server mask")
|
||||||
|
|
||||||
|
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
||||||
|
def testPrivmsgServermask(self):
|
||||||
|
"""
|
||||||
|
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.4.1>
|
||||||
|
<https://datatracker.ietf.org/doc/html/rfc2812>
|
||||||
|
<https://github.com/ircdocs/modern-irc/pull/134>
|
||||||
|
"""
|
||||||
|
self.connectClient("sender", "sender")
|
||||||
|
self.connectClient("user", "user")
|
||||||
|
|
||||||
|
self.sendLine("sender", "OPER operuser operpassword")
|
||||||
|
self.getMessages("sender")
|
||||||
|
|
||||||
|
self.sendLine("sender", "PRIVMSG $*.server :hello there")
|
||||||
|
self.getMessages("sender")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("user"),
|
||||||
|
command="PRIVMSG",
|
||||||
|
params=["$*.server", "hello there"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
||||||
|
def testPrivmsgServermaskNoMatch(self):
|
||||||
|
"""
|
||||||
|
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.4.1>
|
||||||
|
<https://datatracker.ietf.org/doc/html/rfc2812>
|
||||||
|
<https://github.com/ircdocs/modern-irc/pull/134>
|
||||||
|
"""
|
||||||
|
self.connectClient("sender", "sender")
|
||||||
|
self.connectClient("user", "user")
|
||||||
|
|
||||||
|
self.sendLine("sender", "OPER operuser operpassword")
|
||||||
|
self.getMessages("sender")
|
||||||
|
|
||||||
|
self.sendLine("sender", "PRIVMSG $*.foobar :hello there")
|
||||||
|
messages = self.getMessages("sender")
|
||||||
|
self.assertEqual(len(messages), 0, messages)
|
||||||
|
messages = self.getMessages("user")
|
||||||
|
self.assertEqual(len(messages), 0, messages)
|
||||||
|
|
||||||
|
@cases.mark_specifications("Modern")
|
||||||
|
def testPrivmsgServermaskStar(self):
|
||||||
|
"""
|
||||||
|
<https://github.com/ircdocs/modern-irc/pull/134>
|
||||||
|
|
||||||
|
Note: 1459 and 2812 explicitly forbid "$*" as target.
|
||||||
|
"""
|
||||||
|
self.connectClient("sender", "sender")
|
||||||
|
self.connectClient("user", "user")
|
||||||
|
|
||||||
|
self.sendLine("sender", "OPER operuser operpassword")
|
||||||
|
self.getMessages("sender")
|
||||||
|
|
||||||
|
self.connectClient("user", "user")
|
||||||
|
|
||||||
|
self.sendLine("sender", "OPER operuser operpassword")
|
||||||
|
self.getMessages("sender")
|
||||||
|
|
||||||
|
self.sendLine("sender", "PRIVMSG $* :hello there")
|
||||||
|
self.getMessages("sender")
|
||||||
|
self.assertMessageMatch(
|
||||||
|
self.getMessage("user"), command="PRIVMSG", params=["$*", "hello there"]
|
||||||
|
)
|
||||||
|
|
||||||
|
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
||||||
|
def testPrivmsgServermaskNotOper(self):
|
||||||
|
"""
|
||||||
|
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.4.1>
|
||||||
|
<https://datatracker.ietf.org/doc/html/rfc2812>
|
||||||
|
<https://github.com/ircdocs/modern-irc/pull/134>
|
||||||
|
"""
|
||||||
|
self.connectClient("sender", "sender")
|
||||||
|
self.connectClient("user", "user")
|
||||||
|
|
||||||
|
self.sendLine("sender", "PRIVMSG $*.foobar :hello there")
|
||||||
|
self.assertMessageMatch(self.getMessage("sender"), command=ERR_NOPRIVILEGES)
|
||||||
|
|
||||||
|
pms = [msg for msg in self.getMessages("user") if msg.command == "PRIVMSG"]
|
||||||
|
self.assertEqual(len(pms), 0)
|
||||||
|
|
||||||
|
|
||||||
class NoticeTestCase(cases.BaseServerTestCase):
|
class NoticeTestCase(cases.BaseServerTestCase):
|
||||||
@ -72,15 +143,6 @@ class NoticeTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812")
|
@cases.mark_specifications("RFC1459", "RFC2812")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["InspIRCd"],
|
|
||||||
"replies with ERR_NOSUCHCHANNEL to NOTICE to non-existent channels",
|
|
||||||
)
|
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["UnrealIRCd"],
|
|
||||||
"replies with ERR_NOSUCHCHANNEL to NOTICE to non-existent channels: "
|
|
||||||
"https://bugs.unrealircd.org/view.php?id=5949",
|
|
||||||
)
|
|
||||||
def testNoticeNonexistentChannel(self):
|
def testNoticeNonexistentChannel(self):
|
||||||
"""
|
"""
|
||||||
"automatic replies must never be
|
"automatic replies must never be
|
||||||
@ -101,14 +163,6 @@ class NoticeTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
class TagsTestCase(cases.BaseServerTestCase):
|
class TagsTestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_capabilities("message-tags")
|
@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",
|
|
||||||
)
|
|
||||||
def testLineTooLong(self):
|
def testLineTooLong(self):
|
||||||
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)
|
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
|
@ -1,13 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
`Deprecated IRCv3 Metadata <https://ircv3.net/specs/core/metadata-3.2>`_
|
Tests METADATA features.
|
||||||
|
<http://ircv3.net/specs/core/metadata-3.2.html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
|
|
||||||
|
|
||||||
class MetadataTestCase(cases.BaseServerTestCase):
|
class MetadataTestCase(cases.BaseServerTestCase):
|
||||||
valid_metadata_keys = {"display-name", "avatar"}
|
valid_metadata_keys = {"valid_key1", "valid_key2"}
|
||||||
invalid_metadata_keys = {"indisplay-name", "inavatar"}
|
invalid_metadata_keys = {"invalid_key1", "invalid_key2"}
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3", deprecated=True)
|
@cases.mark_specifications("IRCv3", deprecated=True)
|
||||||
def testInIsupport(self):
|
def testInIsupport(self):
|
||||||
@ -36,7 +37,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
def testGetOneUnsetValid(self):
|
def testGetOneUnsetValid(self):
|
||||||
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
|
"""<http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>"""
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.sendLine(1, "METADATA * GET display-name")
|
self.sendLine(1, "METADATA * GET valid_key1")
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
@ -52,7 +53,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
|
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-get>
|
||||||
"""
|
"""
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.sendLine(1, "METADATA * GET display-name avatar")
|
self.sendLine(1, "METADATA * GET valid_key1 valid_key2")
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
m,
|
m,
|
||||||
@ -62,10 +63,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
m.params[1],
|
m.params[1],
|
||||||
"display-name",
|
"valid_key1",
|
||||||
m,
|
m,
|
||||||
fail_msg="Response to “METADATA * GET display-name avatar” "
|
fail_msg="Response to “METADATA * GET valid_key1 valid_key2” "
|
||||||
"did not respond to display-name first: {msg}",
|
"did not respond to valid_key1 first: {msg}",
|
||||||
)
|
)
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
@ -76,10 +77,10 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
m.params[1],
|
m.params[1],
|
||||||
"avatar",
|
"valid_key2",
|
||||||
m,
|
m,
|
||||||
fail_msg="Response to “METADATA * GET display-name avatar” "
|
fail_msg="Response to “METADATA * GET valid_key1 valid_key2” "
|
||||||
"did not respond to avatar as second response: {msg}",
|
"did not respond to valid_key2 as second response: {msg}",
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3", deprecated=True)
|
@cases.mark_specifications("IRCv3", deprecated=True)
|
||||||
@ -135,7 +136,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
m.params[1],
|
m.params[1],
|
||||||
"display-name",
|
"valid_key1",
|
||||||
m,
|
m,
|
||||||
fail_msg="Second param of 761 after setting “{expects}” to "
|
fail_msg="Second param of 761 after setting “{expects}” to "
|
||||||
"“{}” is not “{expects}”: {msg}.",
|
"“{}” is not “{expects}”: {msg}.",
|
||||||
@ -190,7 +191,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
def testSetGetValid(self):
|
def testSetGetValid(self):
|
||||||
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
|
"""<http://ircv3.net/specs/core/metadata-3.2.html>"""
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.assertSetGetValue("*", "display-name", "myvalue")
|
self.assertSetGetValue("*", "valid_key1", "myvalue")
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3", deprecated=True)
|
@cases.mark_specifications("IRCv3", deprecated=True)
|
||||||
def testSetGetZeroCharInValue(self):
|
def testSetGetZeroCharInValue(self):
|
||||||
@ -198,7 +199,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
|
-- <http://ircv3.net/specs/core/metadata-3.2.html#metadata-restrictions>
|
||||||
"""
|
"""
|
||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.assertSetGetValue("*", "display-name", "zero->\0<-zero", "zero->\\0<-zero")
|
self.assertSetGetValue("*", "valid_key1", "zero->\0<-zero", "zero->\\0<-zero")
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3", deprecated=True)
|
@cases.mark_specifications("IRCv3", deprecated=True)
|
||||||
def testSetGetHeartInValue(self):
|
def testSetGetHeartInValue(self):
|
||||||
@ -209,7 +210,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
self.connectClient("foo")
|
self.connectClient("foo")
|
||||||
self.assertSetGetValue(
|
self.assertSetGetValue(
|
||||||
"*",
|
"*",
|
||||||
"display-name",
|
"valid_key1",
|
||||||
"->{}<-".format(heart),
|
"->{}<-".format(heart),
|
||||||
"zero->{}<-zero".format(heart.encode()),
|
"zero->{}<-zero".format(heart.encode()),
|
||||||
)
|
)
|
||||||
@ -223,7 +224,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
# Sending directly because it is not valid UTF-8 so Python would
|
# Sending directly because it is not valid UTF-8 so Python would
|
||||||
# not like it
|
# not like it
|
||||||
self.clients[1].conn.sendall(
|
self.clients[1].conn.sendall(
|
||||||
b"METADATA * SET 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)}
|
commands = {m.command for m in self.getMessages(1)}
|
||||||
self.assertNotIn(
|
self.assertNotIn(
|
||||||
@ -233,7 +234,7 @@ class MetadataTestCase(cases.BaseServerTestCase):
|
|||||||
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
|
"UTF-8 was answered with 761 (RPL_KEYVALUE)",
|
||||||
)
|
)
|
||||||
self.clients[1].conn.sendall(
|
self.clients[1].conn.sendall(
|
||||||
b"METADATA * SET 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)}
|
commands = {m.command for m in self.getMessages(1)}
|
||||||
self.assertNotIn(
|
self.assertNotIn(
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
"""
|
"""
|
||||||
`IRCv3 MONITOR <https://ircv3.net/specs/extensions/monitor>`_
|
<http://ircv3.net/specs/core/monitor-3.2.html>
|
||||||
and `IRCv3 extended-monitor` <https://ircv3.net/specs/extensions/extended-monitor>`_
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
from irctest import cases
|
||||||
|
from irctest.basecontrollers import NotImplementedByController
|
||||||
from irctest import cases, runner
|
|
||||||
from irctest.client_mock import NoMessageException
|
from irctest.client_mock import NoMessageException
|
||||||
from irctest.numerics import (
|
from irctest.numerics import (
|
||||||
RPL_ENDOFMONLIST,
|
RPL_ENDOFMONLIST,
|
||||||
@ -16,10 +14,10 @@ from irctest.numerics import (
|
|||||||
from irctest.patma import ANYSTR, StrRe
|
from irctest.patma import ANYSTR, StrRe
|
||||||
|
|
||||||
|
|
||||||
class _BaseMonitorTestCase(cases.BaseServerTestCase):
|
class MonitorTestCase(cases.BaseServerTestCase):
|
||||||
def check_server_support(self):
|
def check_server_support(self):
|
||||||
if "MONITOR" not in self.server_support:
|
if "MONITOR" not in self.server_support:
|
||||||
raise runner.IsupportTokenNotSupported("MONITOR")
|
raise NotImplementedByController("MONITOR")
|
||||||
|
|
||||||
def assertMononline(self, client, nick, m=None):
|
def assertMononline(self, client, nick, m=None):
|
||||||
if not m:
|
if not m:
|
||||||
@ -45,8 +43,6 @@ class _BaseMonitorTestCase(cases.BaseServerTestCase):
|
|||||||
extra_format=(nick,),
|
extra_format=(nick,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class MonitorTestCase(_BaseMonitorTestCase):
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.mark_isupport("MONITOR")
|
@cases.mark_isupport("MONITOR")
|
||||||
def testMonitorOneDisconnected(self):
|
def testMonitorOneDisconnected(self):
|
||||||
@ -192,12 +188,15 @@ class MonitorTestCase(_BaseMonitorTestCase):
|
|||||||
self.sendLine(1, "MONITOR + *!username@127.0.0.1")
|
self.sendLine(1, "MONITOR + *!username@127.0.0.1")
|
||||||
try:
|
try:
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
self.assertMessageMatch(m, command="731")
|
self.assertNotEqual(
|
||||||
|
m.command,
|
||||||
|
"731",
|
||||||
|
m,
|
||||||
|
fail_msg="Got 731 (RPL_MONOFFLINE) after adding a monitor "
|
||||||
|
"on a mask: {msg}",
|
||||||
|
)
|
||||||
except NoMessageException:
|
except NoMessageException:
|
||||||
pass
|
pass
|
||||||
else:
|
|
||||||
m = self.getMessage(1)
|
|
||||||
self.assertMessageMatch(m, command="731")
|
|
||||||
self.connectClient("bar")
|
self.connectClient("bar")
|
||||||
try:
|
try:
|
||||||
m = self.getMessage(1)
|
m = self.getMessage(1)
|
||||||
@ -249,23 +248,6 @@ class MonitorTestCase(_BaseMonitorTestCase):
|
|||||||
extra_format=(messages,),
|
extra_format=(messages,),
|
||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.mark_isupport("MONITOR")
|
|
||||||
def testMonitorClear(self):
|
|
||||||
"""“Clears the list of targets being monitored. No output will be returned
|
|
||||||
for use of this command.“
|
|
||||||
-- <https://ircv3.net/specs/extensions/monitor#monitor-c>
|
|
||||||
"""
|
|
||||||
self.connectClient("foo")
|
|
||||||
self.check_server_support()
|
|
||||||
self.sendLine(1, "MONITOR + bar")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "MONITOR C")
|
|
||||||
self.sendLine(1, "MONITOR L")
|
|
||||||
m = self.getMessage(1)
|
|
||||||
self.assertEqual(m.command, RPL_ENDOFMONLIST)
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.mark_isupport("MONITOR")
|
@cases.mark_isupport("MONITOR")
|
||||||
def testMonitorList(self):
|
def testMonitorList(self):
|
||||||
@ -301,35 +283,6 @@ class MonitorTestCase(_BaseMonitorTestCase):
|
|||||||
self.sendLine(1, "MONITOR L")
|
self.sendLine(1, "MONITOR L")
|
||||||
checkMonitorSubjects(self.getMessages(1), "bar", {"bazbat"})
|
checkMonitorSubjects(self.getMessages(1), "bar", {"bazbat"})
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.mark_isupport("MONITOR")
|
|
||||||
def testMonitorStatus(self):
|
|
||||||
"""“Outputs for each target in the list being monitored, whether
|
|
||||||
the client is online or offline. All targets that are online will
|
|
||||||
be sent using RPL_MONONLINE, all targets that are offline will be
|
|
||||||
sent using RPL_MONOFFLINE.“
|
|
||||||
-- <https://ircv3.net/specs/extensions/monitor#monitor-s>
|
|
||||||
"""
|
|
||||||
self.connectClient("foo")
|
|
||||||
self.check_server_support()
|
|
||||||
self.connectClient("bar")
|
|
||||||
self.sendLine(1, "MONITOR + bar,baz")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "MONITOR S")
|
|
||||||
msgs = self.getMessages(1)
|
|
||||||
self.assertEqual(
|
|
||||||
len(msgs),
|
|
||||||
2,
|
|
||||||
fail_msg="Expected one RPL_MONONLINE (730) and one RPL_MONOFFLINE (731), got: {}",
|
|
||||||
extra_format=(msgs,),
|
|
||||||
)
|
|
||||||
|
|
||||||
msgs.sort(key=lambda m: m.command)
|
|
||||||
|
|
||||||
self.assertMononline(1, "bar", m=msgs[0])
|
|
||||||
self.assertMonoffline(1, "baz", m=msgs[1])
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.mark_isupport("MONITOR")
|
@cases.mark_isupport("MONITOR")
|
||||||
def testNickChange(self):
|
def testNickChange(self):
|
||||||
@ -346,11 +299,10 @@ class MonitorTestCase(_BaseMonitorTestCase):
|
|||||||
self.sendLine(2, "NICK qux")
|
self.sendLine(2, "NICK qux")
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
mononline = self.getMessages(1)[0]
|
mononline = self.getMessages(1)[0]
|
||||||
self.assertMessageMatch(
|
self.assertEqual(mononline.command, RPL_MONONLINE)
|
||||||
mononline,
|
self.assertEqual(len(mononline.params), 2, mononline.params)
|
||||||
command=RPL_MONONLINE,
|
self.assertIn(mononline.params[0], ("bar", "*"))
|
||||||
params=[StrRe(r"(bar|\*)"), StrRe("qux(!.*)?")],
|
self.assertEqual(mononline.params[1].split("!")[0], "qux")
|
||||||
)
|
|
||||||
|
|
||||||
# no numerics for a case change
|
# no numerics for a case change
|
||||||
self.sendLine(2, "NICK QUX")
|
self.sendLine(2, "NICK QUX")
|
||||||
@ -361,246 +313,7 @@ class MonitorTestCase(_BaseMonitorTestCase):
|
|||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
monoffline = self.getMessages(1)[0]
|
monoffline = self.getMessages(1)[0]
|
||||||
# should get RPL_MONOFFLINE with the current unfolded nick
|
# should get RPL_MONOFFLINE with the current unfolded nick
|
||||||
self.assertMessageMatch(
|
self.assertEqual(monoffline.command, RPL_MONOFFLINE)
|
||||||
monoffline,
|
self.assertEqual(len(monoffline.params), 2, monoffline.params)
|
||||||
command=RPL_MONOFFLINE,
|
self.assertIn(monoffline.params[0], ("bar", "*"))
|
||||||
params=[StrRe(r"(bar|\*)"), "QUX"],
|
self.assertEqual(monoffline.params[1].split("!")[0], "QUX")
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class _BaseExtendedMonitorTestCase(_BaseMonitorTestCase):
|
|
||||||
def _setupExtendedMonitor(self, monitor_before_connect, watcher_caps, watched_caps):
|
|
||||||
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html"""
|
|
||||||
self.connectClient(
|
|
||||||
"foo",
|
|
||||||
capabilities=["extended-monitor", *watcher_caps],
|
|
||||||
skip_if_cap_nak=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if monitor_before_connect:
|
|
||||||
self.sendLine(1, "MONITOR + bar")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.connectClient("bar", capabilities=watched_caps, skip_if_cap_nak=True)
|
|
||||||
self.getMessages(2)
|
|
||||||
else:
|
|
||||||
self.connectClient("bar", capabilities=watched_caps, skip_if_cap_nak=True)
|
|
||||||
self.getMessages(2)
|
|
||||||
self.sendLine(1, "MONITOR + bar")
|
|
||||||
|
|
||||||
self.assertMononline(1, "bar")
|
|
||||||
self.assertEqual(self.getMessages(1), [])
|
|
||||||
|
|
||||||
|
|
||||||
class ExtendedMonitorTestCase(_BaseExtendedMonitorTestCase):
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.mark_capabilities("extended-monitor", "away-notify")
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"monitor_before_connect,cap",
|
|
||||||
[
|
|
||||||
pytest.param(
|
|
||||||
monitor_before_connect,
|
|
||||||
cap,
|
|
||||||
id=("monitor_before_connect" if monitor_before_connect else "")
|
|
||||||
+ "-"
|
|
||||||
+ ("with-cap" if cap else ""),
|
|
||||||
)
|
|
||||||
for monitor_before_connect in [True, False]
|
|
||||||
for cap in [True, False]
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def testExtendedMonitorAway(self, monitor_before_connect, cap):
|
|
||||||
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
|
||||||
with https://ircv3.net/specs/extensions/away-notify
|
|
||||||
"""
|
|
||||||
if cap:
|
|
||||||
self._setupExtendedMonitor(
|
|
||||||
monitor_before_connect, ["away-notify"], ["away-notify"]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._setupExtendedMonitor(monitor_before_connect, ["away-notify"], [])
|
|
||||||
|
|
||||||
self.sendLine(2, "AWAY :afk")
|
|
||||||
self.getMessages(2)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1), nick="bar", command="AWAY", params=["afk"]
|
|
||||||
)
|
|
||||||
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
|
||||||
|
|
||||||
self.sendLine(2, "AWAY")
|
|
||||||
self.getMessages(2)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1), nick="bar", command="AWAY", params=[]
|
|
||||||
)
|
|
||||||
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.mark_capabilities("extended-monitor", "away-notify")
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"monitor_before_connect,cap",
|
|
||||||
[
|
|
||||||
pytest.param(
|
|
||||||
monitor_before_connect,
|
|
||||||
cap,
|
|
||||||
id=("monitor_before_connect" if monitor_before_connect else "")
|
|
||||||
+ "-"
|
|
||||||
+ ("with-cap" if cap else ""),
|
|
||||||
)
|
|
||||||
for monitor_before_connect in [True, False]
|
|
||||||
for cap in [True, False]
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def testExtendedMonitorAwayNoCap(self, monitor_before_connect, cap):
|
|
||||||
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
|
||||||
does nothing when ``away-notify`` is not enabled by the watcher
|
|
||||||
"""
|
|
||||||
if cap:
|
|
||||||
self._setupExtendedMonitor(monitor_before_connect, [], ["away-notify"])
|
|
||||||
else:
|
|
||||||
self._setupExtendedMonitor(monitor_before_connect, [], [])
|
|
||||||
|
|
||||||
self.sendLine(2, "AWAY :afk")
|
|
||||||
self.getMessages(2)
|
|
||||||
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
|
||||||
|
|
||||||
self.sendLine(2, "AWAY")
|
|
||||||
self.getMessages(2)
|
|
||||||
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.mark_capabilities("extended-monitor", "setname")
|
|
||||||
@pytest.mark.parametrize("monitor_before_connect", [True, False])
|
|
||||||
def testExtendedMonitorSetName(self, monitor_before_connect):
|
|
||||||
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
|
||||||
with https://ircv3.net/specs/extensions/setname
|
|
||||||
"""
|
|
||||||
self._setupExtendedMonitor(monitor_before_connect, ["setname"], ["setname"])
|
|
||||||
|
|
||||||
self.sendLine(2, "SETNAME :new name")
|
|
||||||
self.getMessages(2)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1), nick="bar", command="SETNAME", params=["new name"]
|
|
||||||
)
|
|
||||||
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.mark_capabilities("extended-monitor", "setname")
|
|
||||||
@pytest.mark.parametrize("monitor_before_connect", [True, False])
|
|
||||||
def testExtendedMonitorSetNameNoCap(self, monitor_before_connect):
|
|
||||||
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
|
||||||
does nothing when ``setname`` is not enabled by the watcher
|
|
||||||
"""
|
|
||||||
self._setupExtendedMonitor(monitor_before_connect, [], ["setname"])
|
|
||||||
|
|
||||||
self.sendLine(2, "SETNAME :new name")
|
|
||||||
self.getMessages(2)
|
|
||||||
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
|
||||||
class AuthenticatedExtendedMonitorTestCase(_BaseExtendedMonitorTestCase):
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.mark_capabilities("extended-monitor", "account-notify")
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"monitor_before_connect,cap",
|
|
||||||
[
|
|
||||||
pytest.param(
|
|
||||||
monitor_before_connect,
|
|
||||||
cap,
|
|
||||||
id=("monitor_before_connect" if monitor_before_connect else "")
|
|
||||||
+ "-"
|
|
||||||
+ ("with-cap" if cap else ""),
|
|
||||||
)
|
|
||||||
for monitor_before_connect in [True, False]
|
|
||||||
for cap in [True, False]
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def testExtendedMonitorAccountNotify(self, monitor_before_connect, cap):
|
|
||||||
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
|
||||||
does nothing when ``account-notify`` is not enabled by the watcher
|
|
||||||
"""
|
|
||||||
self.controller.registerUser(self, "jilles", "sesame")
|
|
||||||
|
|
||||||
if cap:
|
|
||||||
self._setupExtendedMonitor(
|
|
||||||
monitor_before_connect,
|
|
||||||
["account-notify"],
|
|
||||||
["account-notify", "sasl", "cap-notify"],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._setupExtendedMonitor(
|
|
||||||
monitor_before_connect, ["account-notify"], ["sasl", "cap-notify"]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(2, "AUTHENTICATE PLAIN")
|
|
||||||
m = self.getRegistrationMessage(2)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="AUTHENTICATE",
|
|
||||||
params=["+"],
|
|
||||||
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
|
|
||||||
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
|
|
||||||
)
|
|
||||||
self.sendLine(2, "AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=")
|
|
||||||
m = self.getRegistrationMessage(2)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="900",
|
|
||||||
fail_msg="Did not send 900 after correct SASL authentication.",
|
|
||||||
)
|
|
||||||
self.getMessages(2)
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1), nick="bar", command="ACCOUNT", params=["jilles"]
|
|
||||||
)
|
|
||||||
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.mark_capabilities("extended-monitor", "account-notify")
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"monitor_before_connect,cap",
|
|
||||||
[
|
|
||||||
pytest.param(
|
|
||||||
monitor_before_connect,
|
|
||||||
cap,
|
|
||||||
id=("monitor_before_connect" if monitor_before_connect else "")
|
|
||||||
+ "-"
|
|
||||||
+ ("with-cap" if cap else ""),
|
|
||||||
)
|
|
||||||
for monitor_before_connect in [True, False]
|
|
||||||
for cap in [True, False]
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def testExtendedMonitorAccountNotifyNoCap(self, monitor_before_connect, cap):
|
|
||||||
"""Tests https://ircv3.net/specs/extensions/extended-monitor.html
|
|
||||||
does nothing when ``account-notify`` is not enabled by the watcher
|
|
||||||
"""
|
|
||||||
self.controller.registerUser(self, "jilles", "sesame")
|
|
||||||
|
|
||||||
if cap:
|
|
||||||
self._setupExtendedMonitor(
|
|
||||||
monitor_before_connect, [], ["account-notify", "sasl", "cap-notify"]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self._setupExtendedMonitor(
|
|
||||||
monitor_before_connect, [], ["sasl", "cap-notify"]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(2, "AUTHENTICATE PLAIN")
|
|
||||||
m = self.getRegistrationMessage(2)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="AUTHENTICATE",
|
|
||||||
params=["+"],
|
|
||||||
fail_msg="Sent “AUTHENTICATE PLAIN”, server should have "
|
|
||||||
"replied with “AUTHENTICATE +”, but instead sent: {msg}",
|
|
||||||
)
|
|
||||||
self.sendLine(2, "AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=")
|
|
||||||
m = self.getRegistrationMessage(2)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
m,
|
|
||||||
command="900",
|
|
||||||
fail_msg="Did not send 900 after correct SASL authentication.",
|
|
||||||
)
|
|
||||||
self.getMessages(2)
|
|
||||||
|
|
||||||
self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages")
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""
|
"""
|
||||||
`IRCv3 multi-prefix <https://ircv3.net/specs/extensions/multi-prefix>`_
|
Tests multi-prefix.
|
||||||
|
<http://ircv3.net/specs/extensions/multi-prefix-3.1.html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
@ -15,7 +16,7 @@ class MultiPrefixTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
These prefixes MUST be in order of ‘rank’, from highest to lowest.
|
These prefixes MUST be in order of ‘rank’, from highest to lowest.
|
||||||
"""
|
"""
|
||||||
self.connectClient("foo", capabilities=["multi-prefix"], skip_if_cap_nak=True)
|
self.connectClient("foo", capabilities=["multi-prefix"])
|
||||||
self.joinChannel(1, "#chan")
|
self.joinChannel(1, "#chan")
|
||||||
self.sendLine(1, "MODE #chan +v foo")
|
self.sendLine(1, "MODE #chan +v foo")
|
||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
`Draft IRCv3 multiline <https://ircv3.net/specs/extensions/multiline>`_
|
draft/multiline
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
@ -12,7 +12,7 @@ CONCAT_TAG = "draft/multiline-concat"
|
|||||||
base_caps = ["message-tags", "batch", "echo-message", "server-time", "labeled-response"]
|
base_caps = ["message-tags", "batch", "echo-message", "server-time", "labeled-response"]
|
||||||
|
|
||||||
|
|
||||||
class MultilineTestCase(cases.BaseServerTestCase):
|
class MultilineTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||||
@cases.mark_capabilities("draft/multiline")
|
@cases.mark_capabilities("draft/multiline")
|
||||||
def testBasic(self):
|
def testBasic(self):
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
|
@ -1,114 +1,9 @@
|
|||||||
"""
|
from irctest import cases
|
||||||
The NAMES command (`RFC 1459
|
from irctest.numerics import RPL_ENDOFNAMES
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5>`__,
|
from irctest.patma import ANYSTR
|
||||||
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#names-message>`__)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases, runner
|
|
||||||
from irctest.numerics import RPL_ENDOFNAMES, RPL_NAMREPLY
|
|
||||||
from irctest.patma import ANYSTR, StrRe
|
|
||||||
|
|
||||||
|
|
||||||
class NamesTestCase(cases.BaseServerTestCase):
|
class NamesTestCase(cases.BaseServerTestCase):
|
||||||
def _testNames(self, symbol):
|
|
||||||
self.connectClient("nick1")
|
|
||||||
self.sendLine(1, "JOIN #chan")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.connectClient("nick2")
|
|
||||||
self.sendLine(2, "JOIN #chan")
|
|
||||||
self.getMessages(2)
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "NAMES #chan")
|
|
||||||
|
|
||||||
# TODO: It is technically allowed to have one line for each;
|
|
||||||
# but noone does that.
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=RPL_NAMREPLY,
|
|
||||||
params=[
|
|
||||||
"nick1",
|
|
||||||
*(["="] if symbol else []),
|
|
||||||
"#chan",
|
|
||||||
StrRe("(nick2 @nick1|@nick1 nick2)"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=RPL_ENDOFNAMES,
|
|
||||||
params=["nick1", "#chan", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", deprecated=True)
|
|
||||||
def testNames1459(self):
|
|
||||||
"""
|
|
||||||
https://modern.ircdocs.horse/#names-message
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
|
||||||
"""
|
|
||||||
self._testNames(symbol=False)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
|
||||||
def testNames2812(self):
|
|
||||||
"""
|
|
||||||
https://modern.ircdocs.horse/#names-message
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
|
||||||
"""
|
|
||||||
self._testNames(symbol=True)
|
|
||||||
|
|
||||||
def _testNamesMultipleChannels(self, symbol):
|
|
||||||
self.connectClient("nick1")
|
|
||||||
|
|
||||||
if self.targmax.get("NAMES", "1") == "1":
|
|
||||||
raise runner.OptionalExtensionNotSupported("Multi-target NAMES")
|
|
||||||
|
|
||||||
self.sendLine(1, "JOIN #chan1")
|
|
||||||
self.sendLine(1, "JOIN #chan2")
|
|
||||||
self.getMessages(1)
|
|
||||||
|
|
||||||
self.sendLine(1, "NAMES #chan1,#chan2")
|
|
||||||
|
|
||||||
# TODO: order is unspecified
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=RPL_NAMREPLY,
|
|
||||||
params=["nick1", *(["="] if symbol else []), "#chan1", "@nick1"],
|
|
||||||
)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=RPL_NAMREPLY,
|
|
||||||
params=["nick1", *(["="] if symbol else []), "#chan2", "@nick1"],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=RPL_ENDOFNAMES,
|
|
||||||
params=["nick1", "#chan1,#chan2", ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_isupport("TARGMAX")
|
|
||||||
@cases.mark_specifications("RFC1459", deprecated=True)
|
|
||||||
def testNamesMultipleChannels1459(self):
|
|
||||||
"""
|
|
||||||
https://modern.ircdocs.horse/#names-message
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
|
||||||
"""
|
|
||||||
self._testNamesMultipleChannels(symbol=False)
|
|
||||||
|
|
||||||
@cases.mark_isupport("TARGMAX")
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
|
||||||
def testNamesMultipleChannels2812(self):
|
|
||||||
"""
|
|
||||||
https://modern.ircdocs.horse/#names-message
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
|
||||||
https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
|
||||||
"""
|
|
||||||
self._testNamesMultipleChannels(symbol=True)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
@cases.mark_specifications("RFC1459", "RFC2812", "Modern")
|
||||||
def testNamesInvalidChannel(self):
|
def testNamesInvalidChannel(self):
|
||||||
"""
|
"""
|
||||||
@ -152,101 +47,3 @@ class NamesTestCase(cases.BaseServerTestCase):
|
|||||||
command=RPL_ENDOFNAMES,
|
command=RPL_ENDOFNAMES,
|
||||||
params=["foo", "#nonexisting", ANYSTR],
|
params=["foo", "#nonexisting", ANYSTR],
|
||||||
)
|
)
|
||||||
|
|
||||||
def _testNamesNoArgumentPublic(self, symbol):
|
|
||||||
self.connectClient("nick1")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.sendLine(1, "JOIN #chan1")
|
|
||||||
self.connectClient("nick2")
|
|
||||||
self.sendLine(2, "JOIN #chan2")
|
|
||||||
self.sendLine(2, "MODE #chan2 -sp")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.getMessages(2)
|
|
||||||
|
|
||||||
self.sendLine(1, "NAMES")
|
|
||||||
|
|
||||||
# TODO: order is unspecified
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=RPL_NAMREPLY,
|
|
||||||
params=["nick1", *(["="] if symbol else []), "#chan1", "@nick1"],
|
|
||||||
)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=RPL_NAMREPLY,
|
|
||||||
params=["nick1", *(["="] if symbol else []), "#chan2", "@nick2"],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=RPL_ENDOFNAMES,
|
|
||||||
params=["nick1", ANYSTR, ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", deprecated=True)
|
|
||||||
def testNamesNoArgumentPublic1459(self):
|
|
||||||
"""
|
|
||||||
"If no <channel> parameter is given, a list of all channels and their
|
|
||||||
occupants is returned."
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
|
||||||
"""
|
|
||||||
self._testNamesNoArgumentPublic(symbol=False)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812", deprecated=True)
|
|
||||||
def testNamesNoArgumentPublic2812(self):
|
|
||||||
"""
|
|
||||||
"If no <channel> parameter is given, a list of all channels and their
|
|
||||||
occupants is returned."
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
|
||||||
"""
|
|
||||||
self._testNamesNoArgumentPublic(symbol=True)
|
|
||||||
|
|
||||||
def _testNamesNoArgumentPrivate(self, symbol):
|
|
||||||
self.connectClient("nick1")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.sendLine(1, "JOIN #chan1")
|
|
||||||
self.connectClient("nick2")
|
|
||||||
self.sendLine(2, "JOIN #chan2")
|
|
||||||
self.sendLine(2, "MODE #chan2 +sp")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.getMessages(2)
|
|
||||||
|
|
||||||
self.sendLine(1, "NAMES")
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=RPL_NAMREPLY,
|
|
||||||
params=["nick1", *(["="] if symbol else []), "#chan1", "@nick1"],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command=RPL_ENDOFNAMES,
|
|
||||||
params=["nick1", ANYSTR, ANYSTR],
|
|
||||||
)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459", deprecated=True)
|
|
||||||
def testNamesNoArgumentPrivate1459(self):
|
|
||||||
"""
|
|
||||||
"If no <channel> parameter is given, a list of all channels and their
|
|
||||||
occupants is returned. At the end of this list, a list of users who
|
|
||||||
are visible but either not on any channel or not on a visible channel
|
|
||||||
are listed as being on `channel' "*"."
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
|
||||||
"""
|
|
||||||
self._testNamesNoArgumentPrivate(symbol=False)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812", deprecated=True)
|
|
||||||
def testNamesNoArgumentPrivate2812(self):
|
|
||||||
"""
|
|
||||||
"If no <channel> parameter is given, a list of all channels and their
|
|
||||||
occupants is returned. At the end of this list, a list of users who
|
|
||||||
are visible but either not on any channel or not on a visible channel
|
|
||||||
are listed as being on `channel' "*"."
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5
|
|
||||||
-- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5
|
|
||||||
"""
|
|
||||||
self._testNamesNoArgumentPrivate(symbol=True)
|
|
||||||
|
@ -1,16 +1,6 @@
|
|||||||
"""
|
|
||||||
The PART command (`RFC 1459
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-6.1>`__,
|
|
||||||
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-5.2>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#part-message>`__)
|
|
||||||
|
|
||||||
TODO: cross-reference Modern
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import RPL_NAMREPLY
|
|
||||||
|
|
||||||
|
|
||||||
class PartTestCase(cases.BaseServerTestCase):
|
class PartTestCase(cases.BaseServerTestCase):
|
||||||
@ -85,12 +75,6 @@ class PartTestCase(cases.BaseServerTestCase):
|
|||||||
self.getMessages(1)
|
self.getMessages(1)
|
||||||
self.getMessages(2)
|
self.getMessages(2)
|
||||||
|
|
||||||
self.sendLine(2, "PRIVMSG #chan :hi everyone")
|
|
||||||
self.getMessages(2)
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1), command="PRIVMSG", params=["#chan", "hi everyone"]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "PART #chan")
|
self.sendLine(1, "PART #chan")
|
||||||
# both the PART'ing client and the other channel member should receive
|
# both the PART'ing client and the other channel member should receive
|
||||||
# a PART line:
|
# a PART line:
|
||||||
@ -99,21 +83,6 @@ class PartTestCase(cases.BaseServerTestCase):
|
|||||||
m = self.getMessage(2)
|
m = self.getMessage(2)
|
||||||
self.assertMessageMatch(m, command="PART")
|
self.assertMessageMatch(m, command="PART")
|
||||||
|
|
||||||
self.sendLine(2, "PRIVMSG #chan :hi again everyone")
|
|
||||||
self.getMessages(2)
|
|
||||||
# client 1 has PART'ed and should not receive channel messages:
|
|
||||||
self.assertEqual(self.getMessages(1), [])
|
|
||||||
|
|
||||||
# client 1 should no longer appear in NAMES responses:
|
|
||||||
names = set()
|
|
||||||
self.sendLine(2, "NAMES #chan")
|
|
||||||
for reply in self.getMessages(2):
|
|
||||||
if reply.command != RPL_NAMREPLY:
|
|
||||||
continue
|
|
||||||
names.update(reply.params[-1].replace("@", "").split())
|
|
||||||
self.assertNotIn("bar", names)
|
|
||||||
self.assertIn("baz", names)
|
|
||||||
|
|
||||||
@cases.mark_specifications("RFC2812")
|
@cases.mark_specifications("RFC2812")
|
||||||
def testBasicPartRfc2812(self):
|
def testBasicPartRfc2812(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
"""
|
|
||||||
The PING and PONG commands
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.numerics import ERR_NEEDMOREPARAMS, ERR_NOORIGIN
|
from irctest.numerics import ERR_NEEDMOREPARAMS, ERR_NOORIGIN
|
||||||
from irctest.patma import ANYSTR
|
from irctest.patma import ANYSTR
|
||||||
|
@ -1,12 +1,3 @@
|
|||||||
"""
|
|
||||||
The QUITcommand (`RFC 1459
|
|
||||||
<https://datatracker.ietf.org/doc/html/rfc1459#section-4.1.6>`__,
|
|
||||||
`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.1>`__,
|
|
||||||
`Modern <https://modern.ircdocs.horse/#quit-message>`__)
|
|
||||||
|
|
||||||
TODO: cross-reference RFC 1459 and Modern
|
|
||||||
"""
|
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
@ -16,7 +7,6 @@ from irctest.patma import StrRe
|
|||||||
|
|
||||||
class ChannelQuitTestCase(cases.BaseServerTestCase):
|
class ChannelQuitTestCase(cases.BaseServerTestCase):
|
||||||
@cases.mark_specifications("RFC2812")
|
@cases.mark_specifications("RFC2812")
|
||||||
@cases.xfailIfSoftware(["ircu2", "Nefarious", "snircd"], "ircu2 does not echo QUIT")
|
|
||||||
def testQuit(self):
|
def testQuit(self):
|
||||||
"""“Once a user has joined a channel, he receives information about
|
"""“Once a user has joined a channel, he receives information about
|
||||||
all commands his server receives affecting the channel. This
|
all commands his server receives affecting the channel. This
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
"""
|
|
||||||
`Ergo <https://ergo.chat/>`_-specific tests of responses to DoS attacks
|
|
||||||
using long lines.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
|
|
||||||
|
|
||||||
class ReadqTestCase(cases.BaseServerTestCase):
|
class ReadqTestCase(cases.BaseServerTestCase):
|
||||||
|
"""Test responses to DoS attacks using long lines."""
|
||||||
|
|
||||||
@cases.mark_specifications("Ergo")
|
@cases.mark_specifications("Ergo")
|
||||||
@cases.mark_capabilities("message-tags")
|
@cases.mark_capabilities("message-tags")
|
||||||
def testReadqTags(self):
|
def testReadqTags(self):
|
||||||
|
@ -1,15 +1,9 @@
|
|||||||
"""
|
|
||||||
`Draft IRCv3 account-registration
|
|
||||||
<https://ircv3.net/specs/extensions/account-registration>`_
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.patma import ANYSTR
|
from irctest.patma import ANYSTR
|
||||||
|
|
||||||
REGISTER_CAP_NAME = "draft/account-registration"
|
REGISTER_CAP_NAME = "draft/account-registration"
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
|
class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -34,7 +28,6 @@ class RegisterBeforeConnectTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertMessageMatch(register_response, params=["SUCCESS", ANYSTR, ANYSTR])
|
self.assertMessageMatch(register_response, params=["SUCCESS", ANYSTR, ANYSTR])
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
|
class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -62,7 +55,6 @@ class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
|
class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -113,7 +105,6 @@ class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
|
||||||
@cases.mark_specifications("IRCv3", "Ergo")
|
@cases.mark_specifications("IRCv3", "Ergo")
|
||||||
class RegisterNoLandGrabsTestCase(cases.BaseServerTestCase):
|
class RegisterNoLandGrabsTestCase(cases.BaseServerTestCase):
|
||||||
@staticmethod
|
@staticmethod
|
@ -1,14 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Regression tests for bugs in `Ergo <https://ergo.chat/>`_.
|
Regression tests for bugs in oragono.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from irctest import cases, runner
|
import time
|
||||||
from irctest.numerics import (
|
|
||||||
ERR_ERRONEUSNICKNAME,
|
from irctest import cases
|
||||||
ERR_NICKNAMEINUSE,
|
from irctest.numerics import ERR_ERRONEUSNICKNAME, ERR_NICKNAMEINUSE, RPL_WELCOME
|
||||||
RPL_HELLO,
|
|
||||||
RPL_WELCOME,
|
|
||||||
)
|
|
||||||
from irctest.patma import ANYDICT
|
from irctest.patma import ANYDICT
|
||||||
|
|
||||||
|
|
||||||
@ -60,12 +57,6 @@ class RegressionsTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
@cases.mark_capabilities("message-tags", "batch", "echo-message", "server-time")
|
@cases.mark_capabilities("message-tags", "batch", "echo-message", "server-time")
|
||||||
def testTagCap(self):
|
def testTagCap(self):
|
||||||
if self.controller.software_name == "UnrealIRCd":
|
|
||||||
raise runner.ImplementationChoice(
|
|
||||||
"Arbitrary +draft/reply values (TODO: adapt this test to use real "
|
|
||||||
"values so their pass Unreal's validation) "
|
|
||||||
"https://bugs.unrealircd.org/view.php?id=5948"
|
|
||||||
)
|
|
||||||
# regression test for oragono #754
|
# regression test for oragono #754
|
||||||
self.connectClient(
|
self.connectClient(
|
||||||
"alice",
|
"alice",
|
||||||
@ -108,13 +99,13 @@ class RegressionsTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("RFC1459")
|
@cases.mark_specifications("RFC1459")
|
||||||
@cases.xfailIfSoftware(["ngIRCd"], "wat")
|
|
||||||
def testStarNick(self):
|
def testStarNick(self):
|
||||||
self.addClient(1)
|
self.addClient(1)
|
||||||
self.sendLine(1, "NICK *")
|
self.sendLine(1, "NICK *")
|
||||||
self.sendLine(1, "USER u s e r")
|
self.sendLine(1, "USER u s e r")
|
||||||
replies = {"NOTICE"}
|
replies = {"NOTICE"}
|
||||||
while replies <= {"NOTICE", RPL_HELLO}:
|
time.sleep(2) # give time to slow servers, like irc2 to reply
|
||||||
|
while replies == {"NOTICE"}:
|
||||||
replies = set(msg.command for msg in self.getMessages(1, synchronize=False))
|
replies = set(msg.command for msg in self.getMessages(1, synchronize=False))
|
||||||
self.assertIn(ERR_ERRONEUSNICKNAME, replies)
|
self.assertIn(ERR_ERRONEUSNICKNAME, replies)
|
||||||
self.assertNotIn(RPL_WELCOME, replies)
|
self.assertNotIn(RPL_WELCOME, replies)
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
"""
|
|
||||||
RELAYMSG command of `Ergo <https://ergo.chat/>`_
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.irc_utils.junkdrawer import random_name
|
from irctest.irc_utils.junkdrawer import random_name
|
||||||
from irctest.patma import ANYSTR
|
from irctest.patma import ANYSTR
|
||||||
|
@ -1,7 +1,3 @@
|
|||||||
"""
|
|
||||||
Roleplay features of `Ergo <https://ergo.chat/>`_
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
from irctest import cases
|
||||||
from irctest.irc_utils.junkdrawer import random_name
|
from irctest.irc_utils.junkdrawer import random_name
|
||||||
from irctest.numerics import ERR_CANNOTSENDRP
|
from irctest.numerics import ERR_CANNOTSENDRP
|
||||||
|
@ -12,9 +12,9 @@ class RegistrationTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
|
|
||||||
@cases.mark_services
|
@cases.mark_services
|
||||||
class SaslTestCase(cases.BaseServerTestCase):
|
class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
def testPlain(self):
|
def testPlain(self):
|
||||||
"""PLAIN authentication with correct username/password."""
|
"""PLAIN authentication with correct username/password."""
|
||||||
self.controller.registerUser(self, "foo", "sesame")
|
self.controller.registerUser(self, "foo", "sesame")
|
||||||
@ -54,7 +54,7 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
def testPlainNonAscii(self):
|
def testPlainNonAscii(self):
|
||||||
password = "é" * 100
|
password = "é" * 100
|
||||||
authstring = base64.b64encode(
|
authstring = base64.b64encode(
|
||||||
@ -82,7 +82,7 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
def testPlainNoAuthzid(self):
|
def testPlainNoAuthzid(self):
|
||||||
"""“message = [authzid] UTF8NUL authcid UTF8NUL passwd
|
"""“message = [authzid] UTF8NUL authcid UTF8NUL passwd
|
||||||
|
|
||||||
@ -170,22 +170,7 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
@cases.xfailIf(
|
|
||||||
lambda self: (
|
|
||||||
self.controller.services_controller is not None
|
|
||||||
and self.controller.services_controller.software_name == "Anope"
|
|
||||||
),
|
|
||||||
"Anope does not handle split AUTHENTICATE (reported on IRC)",
|
|
||||||
)
|
|
||||||
@cases.xfailIf(
|
|
||||||
lambda self: (
|
|
||||||
self.controller.services_controller is not None
|
|
||||||
and self.controller.services_controller.software_name == "Dlk-Services"
|
|
||||||
),
|
|
||||||
"Dlk does not handle split AUTHENTICATE "
|
|
||||||
"https://github.com/DalekIRC/Dalek-Services/issues/28",
|
|
||||||
)
|
|
||||||
def testPlainLarge(self):
|
def testPlainLarge(self):
|
||||||
"""Test the client splits large AUTHENTICATE messages whose payload
|
"""Test the client splits large AUTHENTICATE messages whose payload
|
||||||
is not a multiple of 400.
|
is not a multiple of 400.
|
||||||
@ -247,14 +232,7 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||||||
# message's length too big for it to be valid.
|
# message's length too big for it to be valid.
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.skipUnlessHasMechanism("PLAIN")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN")
|
||||||
@cases.xfailIf(
|
|
||||||
lambda self: (
|
|
||||||
self.controller.services_controller is not None
|
|
||||||
and self.controller.services_controller.software_name == "Anope"
|
|
||||||
),
|
|
||||||
"Anope does not handle split AUTHENTICATE (reported on IRC)",
|
|
||||||
)
|
|
||||||
def testPlainLargeEquals400(self):
|
def testPlainLargeEquals400(self):
|
||||||
"""Test the client splits large AUTHENTICATE messages whose payload
|
"""Test the client splits large AUTHENTICATE messages whose payload
|
||||||
is not a multiple of 400.
|
is not a multiple of 400.
|
||||||
@ -299,7 +277,7 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||||||
# message's length too big for it to be valid.
|
# message's length too big for it to be valid.
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256")
|
||||||
def testScramSha256Success(self):
|
def testScramSha256Success(self):
|
||||||
self.controller.registerUser(self, "Scramtest", "sesame")
|
self.controller.registerUser(self, "Scramtest", "sesame")
|
||||||
|
|
||||||
@ -355,7 +333,7 @@ class SaslTestCase(cases.BaseServerTestCase):
|
|||||||
self.confirmSuccessfulAuth()
|
self.confirmSuccessfulAuth()
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
@cases.mark_specifications("IRCv3")
|
||||||
@cases.skipUnlessHasMechanism("SCRAM-SHA-256")
|
@cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256")
|
||||||
def testScramSha256Failure(self):
|
def testScramSha256Failure(self):
|
||||||
self.controller.registerUser(self, "Scramtest", "sesame")
|
self.controller.registerUser(self, "Scramtest", "sesame")
|
||||||
|
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
"""
|
|
||||||
`IRCv3 SETNAME<https://ircv3.net/specs/extensions/setname>`_
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases
|
|
||||||
from irctest.numerics import RPL_WHOISUSER
|
|
||||||
|
|
||||||
|
|
||||||
class SetnameMessageTestCase(cases.BaseServerTestCase):
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.mark_capabilities("setname")
|
|
||||||
def testSetnameMessage(self):
|
|
||||||
self.connectClient("foo", capabilities=["setname"], skip_if_cap_nak=True)
|
|
||||||
|
|
||||||
self.sendLine(1, "SETNAME bar")
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command="SETNAME",
|
|
||||||
params=["bar"],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.sendLine(1, "WHOIS foo")
|
|
||||||
whoisuser = [m for m in self.getMessages(1) if m.command == RPL_WHOISUSER][0]
|
|
||||||
self.assertEqual(whoisuser.params[-1], "bar")
|
|
||||||
|
|
||||||
@cases.mark_specifications("IRCv3")
|
|
||||||
@cases.mark_capabilities("setname")
|
|
||||||
def testSetnameChannel(self):
|
|
||||||
"""“[Servers] MUST send the server-to-client version of the
|
|
||||||
SETNAME message to all clients in common channels, as well as
|
|
||||||
to the client from which it originated, to confirm the change
|
|
||||||
has occurred.
|
|
||||||
|
|
||||||
The SETNAME message MUST NOT be sent to clients which do not
|
|
||||||
have the setname capability negotiated.“
|
|
||||||
"""
|
|
||||||
self.connectClient("foo", capabilities=["setname"], skip_if_cap_nak=True)
|
|
||||||
self.connectClient("bar", capabilities=["setname"], skip_if_cap_nak=True)
|
|
||||||
self.connectClient("baz")
|
|
||||||
|
|
||||||
self.joinChannel(1, "#chan")
|
|
||||||
self.joinChannel(2, "#chan")
|
|
||||||
self.joinChannel(3, "#chan")
|
|
||||||
self.getMessages(1)
|
|
||||||
self.getMessages(2)
|
|
||||||
self.getMessages(3)
|
|
||||||
|
|
||||||
self.sendLine(1, "SETNAME qux")
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(1),
|
|
||||||
command="SETNAME",
|
|
||||||
params=["qux"],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertMessageMatch(
|
|
||||||
self.getMessage(2),
|
|
||||||
command="SETNAME",
|
|
||||||
params=["qux"],
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
self.getMessages(3),
|
|
||||||
[],
|
|
||||||
"Got SETNAME response when it was not negotiated",
|
|
||||||
)
|
|
@ -1,10 +1,3 @@
|
|||||||
"""
|
|
||||||
STATUSMSG ISUPPORT token and related PRIVMSG (`Modern
|
|
||||||
<https://modern.ircdocs.horse/#statusmsg-parameter>`__)
|
|
||||||
|
|
||||||
TODO: cross-reference Modern
|
|
||||||
"""
|
|
||||||
|
|
||||||
from irctest import cases, runner
|
from irctest import cases, runner
|
||||||
from irctest.numerics import RPL_NAMREPLY
|
from irctest.numerics import RPL_NAMREPLY
|
||||||
|
|
||||||
@ -17,11 +10,6 @@ class StatusmsgTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertEqual(self.server_support["STATUSMSG"], "~&@%+")
|
self.assertEqual(self.server_support["STATUSMSG"], "~&@%+")
|
||||||
|
|
||||||
@cases.mark_isupport("STATUSMSG")
|
@cases.mark_isupport("STATUSMSG")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["ircu2", "Nefarious", "snircd"],
|
|
||||||
"STATUSMSG is present in ISUPPORT, but it not actually supported as PRIVMSG "
|
|
||||||
"target (only for WALLCOPS/WALLCHOPS/...)",
|
|
||||||
)
|
|
||||||
def testStatusmsgFromOp(self):
|
def testStatusmsgFromOp(self):
|
||||||
"""Test that STATUSMSG are sent to the intended recipients,
|
"""Test that STATUSMSG are sent to the intended recipients,
|
||||||
with the intended prefixes."""
|
with the intended prefixes."""
|
||||||
@ -73,11 +61,6 @@ class StatusmsgTestCase(cases.BaseServerTestCase):
|
|||||||
self.assertEqual(len(unprivilegedMessages), 0)
|
self.assertEqual(len(unprivilegedMessages), 0)
|
||||||
|
|
||||||
@cases.mark_isupport("STATUSMSG")
|
@cases.mark_isupport("STATUSMSG")
|
||||||
@cases.xfailIfSoftware(
|
|
||||||
["ircu2", "Nefarious", "snircd"],
|
|
||||||
"STATUSMSG is present in ISUPPORT, but it not actually supported as PRIVMSG "
|
|
||||||
"target (only for WALLCOPS/WALLCHOPS/...)",
|
|
||||||
)
|
|
||||||
def testStatusmsgFromRegular(self):
|
def testStatusmsgFromRegular(self):
|
||||||
"""Test that STATUSMSG are sent to the intended recipients,
|
"""Test that STATUSMSG are sent to the intended recipients,
|
||||||
with the intended prefixes."""
|
with the intended prefixes."""
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
import math
|
|
||||||
import time
|
|
||||||
|
|
||||||
from irctest import cases
|
|
||||||
from irctest.numerics import RPL_TIME
|
|
||||||
from irctest.patma import ANYSTR, StrRe
|
|
||||||
|
|
||||||
|
|
||||||
class TimeTestCase(cases.BaseServerTestCase):
|
|
||||||
def testTime(self):
|
|
||||||
self.connectClient("user")
|
|
||||||
|
|
||||||
time_before = math.floor(time.time())
|
|
||||||
self.sendLine(1, "TIME")
|
|
||||||
|
|
||||||
msg = self.getMessage(1)
|
|
||||||
|
|
||||||
time_after = math.ceil(time.time())
|
|
||||||
|
|
||||||
if len(msg.params) == 5:
|
|
||||||
# ircu2, snircd
|
|
||||||
self.assertMessageMatch(
|
|
||||||
msg,
|
|
||||||
command=RPL_TIME,
|
|
||||||
params=["user", "My.Little.Server", StrRe("[0-9]+"), "0", ANYSTR],
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
int(msg.params[2]),
|
|
||||||
range(time_before, time_after + 1),
|
|
||||||
"Timestamp not in expected range",
|
|
||||||
)
|
|
||||||
elif len(msg.params) == 4:
|
|
||||||
# bahamut
|
|
||||||
self.assertMessageMatch(
|
|
||||||
msg,
|
|
||||||
command=RPL_TIME,
|
|
||||||
params=["user", "My.Little.Server", StrRe("[0-9]+"), ANYSTR],
|
|
||||||
)
|
|
||||||
self.assertIn(
|
|
||||||
int(msg.params[2]),
|
|
||||||
range(time_before, time_after + 1),
|
|
||||||
"Timestamp not in expected range",
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Common case
|
|
||||||
self.assertMessageMatch(
|
|
||||||
msg, command=RPL_TIME, params=["user", "My.Little.Server", ANYSTR]
|
|
||||||
)
|
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user