diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bab7e036..bb1fb60c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,11 +7,6 @@ on: permissions: contents: read -env: - FORCE_COLOR: 1 - PYTHONUNBUFFERED: 1 - PYTHONUTF8: 1 - jobs: smoke-tests: if: github.event.pull_request.draft == false @@ -25,18 +20,18 @@ jobs: - name: Check scripts in repository are executable run: | IFS=$'\n'; - for f in $(find . -name '*.sh'); do if [[ ! -x $f ]]; then echo "$f is not executable" && FAIL=1; fi ;done + for f in $(find . -name '*.sh' -o -name '*.bats'); do if [[ ! -x $f ]]; then echo "$f is not executable" && FAIL=1; fi ;done unset IFS; # If FAIL is 1 then we fail. [[ $FAIL == 1 ]] && exit 1 || echo "Scripts are executable!" - name: Differential ShellCheck + if: github.event_name == 'pull_request' uses: redhat-plumbers-in-action/differential-shellcheck@d965e66ec0b3b2f821f75c8eff9b12442d9a7d1e #v5.5.6 with: severity: warning display-engine: sarif-fmt - - name: Spell-Checking uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 #v2.2 with: @@ -48,12 +43,6 @@ jobs: - name: Run editorconfig-checker run: editorconfig-checker - - name: Check python code formatting with black - uses: psf/black@c6755bb741b6481d6b3d3bb563c83fa060db96c9 #26.3.1 - with: - src: "./test" - options: "--check --diff --color" - distro-test: if: github.event.pull_request.draft == false runs-on: ubuntu-latest @@ -79,22 +68,9 @@ jobs: alpine_3_22, alpine_3_23, ] - env: - DISTRO: ${{matrix.distro}} steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 - - name: Set up Python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 #v6.2.0 - with: - python-version: "3.13" - - - name: Install wheel - run: pip install wheel - - - name: Install dependencies - run: pip install -r test/requirements.txt - - - name: Test with tox - run: tox -c test/tox.${DISTRO}.ini + - name: Run BATS test suite for ${{ matrix.distro }} + run: DISTRO=${{ matrix.distro }} bash test/run.sh diff --git a/.gitignore b/.gitignore index 6322fd3e..97748824 100644 --- a/.gitignore +++ b/.gitignore @@ -1,15 +1,7 @@ .DS_Store -*.pyc *.swp -__pycache__ -.cache -.pytest_cache -.tox -.eggs -*.egg-info .idea/ *.iml .vscode/ -.venv/ .fleet/ -.cache/ +test/libs/ diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/test/conftest.py b/test/conftest.py deleted file mode 100644 index d4c763e7..00000000 --- a/test/conftest.py +++ /dev/null @@ -1,175 +0,0 @@ -import pytest -import testinfra -import testinfra.backend.docker -import subprocess -from textwrap import dedent - -IMAGE = "pytest_pihole:test_container" -tick_box = "[✓]" -cross_box = "[✗]" -info_box = "[i]" - - -# Monkeypatch sh to bash, if they ever support non hard code /bin/sh this can go away -# https://github.com/pytest-dev/pytest-testinfra/blob/master/testinfra/backend/docker.py -def run_bash(self, command, *args, **kwargs): - cmd = self.get_command(command, *args) - if self.user is not None: - out = self.run_local( - "docker exec -u %s %s /bin/bash -c %s", self.user, self.name, cmd - ) - else: - out = self.run_local("docker exec %s /bin/bash -c %s", self.name, cmd) - out.command = self.encode(cmd) - return out - - -testinfra.backend.docker.DockerBackend.run = run_bash - - -@pytest.fixture -def host(): - # run a container - docker_id = ( - subprocess.check_output(["docker", "run", "-t", "-d", "--cap-add=ALL", IMAGE]) - .decode() - .strip() - ) - - # return a testinfra connection to the container - docker_host = testinfra.get_host("docker://" + docker_id) - - yield docker_host - # at the end of the test suite, destroy the container - subprocess.check_call(["docker", "rm", "-f", docker_id]) - - -# Helper functions -def mock_command(script, args, container): - """ - Allows for setup of commands we don't really want to have to run for real - in unit tests - """ - full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent(r"""\ - #!/bin/bash -e - echo "\$0 \$@" >> /var/log/{script} - case "\$1" in""".format(script=script)) - for k, v in args.items(): - case = dedent(""" - {arg}) - echo {res} - exit {retcode} - ;;""".format(arg=k, res=v[0], retcode=v[1])) - mock_script += case - mock_script += dedent(""" - esac""") - container.run( - """ - cat < {script}\n{content}\nEOF - chmod +x {script} - rm -f /var/log/{scriptlog}""".format( - script=full_script_path, content=mock_script, scriptlog=script - ) - ) - - -def mock_command_passthrough(script, args, container): - """ - Per other mock_command* functions, allows intercepting of commands we don't want to run for real - in unit tests, however also allows only specific arguments to be mocked. Anything not defined will - be passed through to the actual command. - - Example use-case: mocking `git pull` but still allowing `git clone` to work as intended - """ - orig_script_path = container.check_output("command -v {}".format(script)) - full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent(r"""\ - #!/bin/bash -e - echo "\$0 \$@" >> /var/log/{script} - case "\$1" in""".format(script=script)) - for k, v in args.items(): - case = dedent(""" - {arg}) - echo {res} - exit {retcode} - ;;""".format(arg=k, res=v[0], retcode=v[1])) - mock_script += case - mock_script += dedent(r""" - *) - {orig_script_path} "\$@" - ;;""".format(orig_script_path=orig_script_path)) - mock_script += dedent(""" - esac""") - container.run( - """ - cat < {script}\n{content}\nEOF - chmod +x {script} - rm -f /var/log/{scriptlog}""".format( - script=full_script_path, content=mock_script, scriptlog=script - ) - ) - - -def mock_command_run(script, args, container): - """ - Allows for setup of commands we don't really want to have to run for real - in unit tests - """ - full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent(r"""\ - #!/bin/bash -e - echo "\$0 \$@" >> /var/log/{script} - case "\$1 \$2" in""".format(script=script)) - for k, v in args.items(): - case = dedent(""" - \"{arg}\") - echo {res} - exit {retcode} - ;;""".format(arg=k, res=v[0], retcode=v[1])) - mock_script += case - mock_script += dedent(r""" - esac""") - container.run( - """ - cat < {script}\n{content}\nEOF - chmod +x {script} - rm -f /var/log/{scriptlog}""".format( - script=full_script_path, content=mock_script, scriptlog=script - ) - ) - - -def mock_command_2(script, args, container): - """ - Allows for setup of commands we don't really want to have to run for real - in unit tests - """ - full_script_path = "/usr/local/bin/{}".format(script) - mock_script = dedent(r"""\ - #!/bin/bash -e - echo "\$0 \$@" >> /var/log/{script} - case "\$1 \$2" in""".format(script=script)) - for k, v in args.items(): - case = dedent(""" - \"{arg}\") - echo \"{res}\" - exit {retcode} - ;;""".format(arg=k, res=v[0], retcode=v[1])) - mock_script += case - mock_script += dedent(r""" - esac""") - container.run( - """ - cat < {script}\n{content}\nEOF - chmod +x {script} - rm -f /var/log/{scriptlog}""".format( - script=full_script_path, content=mock_script, scriptlog=script - ) - ) - - -def run_script(Pihole, script): - result = Pihole.run(script) - assert result.rc == 0 - return result diff --git a/test/helpers/mocks.bash b/test/helpers/mocks.bash new file mode 100755 index 00000000..33f9f842 --- /dev/null +++ b/test/helpers/mocks.bash @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# Mock command helpers for BATS tests. +# +# These are the BATS equivalents of the mock_command* functions in conftest.py. +# Each function writes a bash case-statement script to /usr/local/bin/ +# inside the container, allowing tests to intercept command invocations. +# +# Usage: +# mock_command CONTAINER SCRIPT ARG1 OUTPUT1 RC1 [ARG2 OUTPUT2 RC2 ...] +# mock_command_2 CONTAINER SCRIPT ARG1 OUTPUT1 RC1 [ARG2 OUTPUT2 RC2 ...] +# mock_command_passthrough CONTAINER SCRIPT ARG1 OUTPUT1 RC1 [...] +# +# mock_command: matches on $1 (first argument); unquoted case pattern +# mock_command_2: matches on "$1 $2" (first two args joined); quoted pattern +# mock_command_passthrough: like mock_command but falls through to real binary +# +# Use '*' as ARG for a catch-all case (only works in mock_command and +# mock_command_passthrough; in mock_command_2 it matches the literal string '*'). +# +# Content is transferred to the container via base64 to avoid quoting issues. + +_write_mock_to_container() { + local container="$1" script_name="$2" script_content="$3" + # base64 alphabet is [A-Za-z0-9+/=] — safe to single-quote in the shell + local encoded + encoded=$(printf '%s' "$script_content" | base64 | tr -d '\n') + docker exec "$container" bash -c \ + "printf '%s' '${encoded}' | base64 -d > /usr/local/bin/${script_name} && chmod +x /usr/local/bin/${script_name} && rm -f /var/log/${script_name}" +} + +# mock_command — matches on $1 +mock_command() { + local container="$1" script_name="$2" + shift 2 + + local script + script='#!/bin/bash -e'$'\n' + script+="echo \"\$0 \$@\" >> /var/log/${script_name}"$'\n' + script+='case "$1" in'$'\n' + + while (( $# >= 3 )); do + local arg="$1" output="$2" rc="$3" + shift 3 + script+=" ${arg})"$'\n' + script+=" echo ${output}"$'\n' + script+=" exit ${rc}"$'\n' + script+=" ;;"$'\n' + done + script+='esac'$'\n' + + _write_mock_to_container "$container" "$script_name" "$script" +} + +# mock_command_2 — matches on "$1 $2" (quoted pattern, quoted echo output) +mock_command_2() { + local container="$1" script_name="$2" + shift 2 + + local script + script='#!/bin/bash -e'$'\n' + script+="echo \"\$0 \$@\" >> /var/log/${script_name}"$'\n' + script+='case "$1 $2" in'$'\n' + + while (( $# >= 3 )); do + local arg="$1" output="$2" rc="$3" + shift 3 + script+=" \"${arg}\")"$'\n' + script+=" echo \"${output}\""$'\n' + script+=" exit ${rc}"$'\n' + script+=" ;;"$'\n' + done + script+='esac'$'\n' + + _write_mock_to_container "$container" "$script_name" "$script" +} + +# mock_command_passthrough — matches on $1; falls through to real binary for +# unmatched arguments +mock_command_passthrough() { + local container="$1" script_name="$2" + shift 2 + + # Find the real binary path before we shadow it + local orig_path + orig_path=$(docker exec "$container" bash -c "command -v ${script_name}") + + local script + script='#!/bin/bash -e'$'\n' + script+="echo \"\$0 \$@\" >> /var/log/${script_name}"$'\n' + script+='case "$1" in'$'\n' + + while (( $# >= 3 )); do + local arg="$1" output="$2" rc="$3" + shift 3 + script+=" ${arg})"$'\n' + script+=" echo ${output}"$'\n' + script+=" exit ${rc}"$'\n' + script+=" ;;"$'\n' + done + script+=' *)'$'\n' + script+=" ${orig_path} \"\$@\""$'\n' + script+=' ;;'$'\n' + script+='esac'$'\n' + + _write_mock_to_container "$container" "$script_name" "$script" +} diff --git a/test/requirements.txt b/test/requirements.txt deleted file mode 100644 index c8feda47..00000000 --- a/test/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -pyyaml == 6.0.3 -pytest == 9.0.2 -pytest-xdist == 3.8.0 -pytest-testinfra == 10.2.2 -tox == 4.49.1 -pytest-clarity == 1.0.1 diff --git a/test/run.sh b/test/run.sh new file mode 100755 index 00000000..dac34c7e --- /dev/null +++ b/test/run.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# --------------------------------------------------------------------------- +# Distro selection +# --------------------------------------------------------------------------- + +if [[ -z "${DISTRO:-}" ]]; then + echo "Error: DISTRO is required." + echo "Example: DISTRO=debian_12 bash test/run.sh" + echo "" + echo "Available distros:" + ls _*.Dockerfile | sed 's/^_//;s/\.Dockerfile$//' | sort + exit 1 +fi + +DOCKERFILE="_${DISTRO}.Dockerfile" +if [[ ! -f "$DOCKERFILE" ]]; then + echo "Error: Dockerfile not found: $DOCKERFILE" + exit 1 +fi + +# Determine distro family to select which test files to run. +# rhel: CentOS/Fedora — includes SELinux tests +# alpine: Alpine Linux +# debian: Debian/Ubuntu (default) +distro_family() { + case "$1" in + centos_* | fedora_*) echo "rhel" ;; + alpine_*) echo "alpine" ;; + *) echo "debian" ;; + esac +} +DISTRO_FAMILY=$(distro_family "$DISTRO") + +# --------------------------------------------------------------------------- +# Build the test image +# --------------------------------------------------------------------------- + +IMAGE_TAG="pihole_test:${DISTRO}" + +docker buildx build \ + --load \ + --progress plain \ + -f "$DOCKERFILE" \ + -t "$IMAGE_TAG" \ + ../ + +# --------------------------------------------------------------------------- +# Install BATS and helper libraries (on-demand, not committed) +# --------------------------------------------------------------------------- + +mkdir -p libs +if [[ ! -d libs/bats ]]; then + echo "Cloning bats-core..." + git clone --depth=1 --quiet https://github.com/bats-core/bats-core libs/bats +fi +if [[ ! -d libs/bats-support ]]; then + echo "Cloning bats-support..." + git clone --depth=1 --quiet https://github.com/bats-core/bats-support libs/bats-support +fi +if [[ ! -d libs/bats-assert ]]; then + echo "Cloning bats-assert..." + git clone --depth=1 --quiet https://github.com/bats-core/bats-assert libs/bats-assert +fi + +BATS="${BATS:-libs/bats/bin/bats}" + +# --------------------------------------------------------------------------- +# Run tests +# --------------------------------------------------------------------------- + +export IMAGE_TAG DISTRO DISTRO_FAMILY + +TEST_FILES=(test_automated_install.bats test_utils.bats) +[[ "$DISTRO_FAMILY" == "rhel" ]] && TEST_FILES+=(test_selinux.bats) + +# Use pretty output only when stdout is a real terminal; fall back to TAP in CI +BATS_FLAGS=() +[[ -t 1 ]] && BATS_FLAGS+=("-p") +"$BATS" "${BATS_FLAGS[@]}" "${TEST_FILES[@]}" diff --git a/test/setup.py b/test/setup.py deleted file mode 100644 index cdde20d3..00000000 --- a/test/setup.py +++ /dev/null @@ -1,7 +0,0 @@ -from setuptools import setup - -setup( - py_modules=[], - setup_requires=["pytest-runner"], - tests_require=["pytest"], -) diff --git a/test/test_any_automated_install.py b/test/test_any_automated_install.py deleted file mode 100644 index aa48fd32..00000000 --- a/test/test_any_automated_install.py +++ /dev/null @@ -1,472 +0,0 @@ -import pytest -from textwrap import dedent -import re -from .conftest import ( - tick_box, - info_box, - cross_box, - mock_command, - mock_command_2, - mock_command_passthrough, -) - -FTL_BRANCH = "development" - - -def test_supported_package_manager(host): - """ - confirm installer exits when no supported package manager found - """ - # break supported package managers - host.run("rm -rf /usr/bin/apt-get") - host.run("rm -rf /usr/bin/rpm") - host.run("rm -rf /sbin/apk") - package_manager_detect = host.run(""" - source /opt/pihole/basic-install.sh - package_manager_detect - """) - expected_stdout = cross_box + " No supported package manager found" - assert expected_stdout in package_manager_detect.stdout - # assert package_manager_detect.rc == 1 - - -def test_selinux_not_detected(host): - """ - confirms installer continues when SELinux configuration file does not exist - """ - check_selinux = host.run(""" - rm -f /etc/selinux/config - source /opt/pihole/basic-install.sh - checkSelinux - """) - expected_stdout = info_box + " SELinux not detected" - assert expected_stdout in check_selinux.stdout - assert check_selinux.rc == 0 - - -def get_directories_recursive(host, directory): - if directory is None: - return directory - # returns all non-hidden subdirs of 'directory' - dirs_raw = host.run("find {} -type d -not -path '*/.*'".format(directory)) - dirs = list(filter(bool, dirs_raw.stdout.splitlines())) - return dirs - - -def test_installPihole_fresh_install_readableFiles(host): - """ - confirms all necessary files are readable by pihole user - """ - # dialog returns Cancel for user prompt - mock_command("dialog", {"*": ("", "0")}, host) - # mock git pull - mock_command_passthrough("git", {"pull": ("", "0")}, host) - # mock systemctl to not start FTL - mock_command_2( - "systemctl", - { - "enable pihole-FTL": ("", "0"), - "restart pihole-FTL": ("", "0"), - "start pihole-FTL": ("", "0"), - "*": ('echo "systemctl call with $@"', "0"), - }, - host, - ) - mock_command_2( - "rc-service", - { - "rc-service pihole-FTL enable": ("", "0"), - "rc-service pihole-FTL restart": ("", "0"), - "rc-service pihole-FTL start": ("", "0"), - "*": ('echo "rc-service call with $@"', "0"), - }, - host, - ) - # try to install man - host.run("command -v apt-get > /dev/null && apt-get install -qq man") - host.run("command -v dnf > /dev/null && dnf install -y man") - host.run("command -v yum > /dev/null && yum install -y man") - host.run("command -v apk > /dev/null && apk add mandoc man-pages") - # Workaround to get FTLv6 installed until it reaches master branch - host.run('echo "' + FTL_BRANCH + '" > /etc/pihole/ftlbranch') - install = host.run(""" - export TERM=xterm - export DEBIAN_FRONTEND=noninteractive - umask 0027 - runUnattended=true - source /opt/pihole/basic-install.sh > /dev/null - runUnattended=true - main - /opt/pihole/pihole-FTL-prestart.sh - """) - assert 0 == install.rc - maninstalled = True - if (info_box + " man not installed") in install.stdout: - maninstalled = False - if (info_box + " man pages not installed") in install.stdout: - maninstalled = False - piholeuser = "pihole" - exit_status_success = 0 - test_cmd = 'su -s /bin/bash -c "test -{0} {1}" -p {2}' - # check files in /etc/pihole for read, write and execute permission - check_etc = test_cmd.format("r", "/etc/pihole", piholeuser) - actual_rc = host.run(check_etc).rc - assert exit_status_success == actual_rc - check_etc = test_cmd.format("x", "/etc/pihole", piholeuser) - actual_rc = host.run(check_etc).rc - assert exit_status_success == actual_rc - # readable and writable dhcp.leases - check_leases = test_cmd.format("r", "/etc/pihole/dhcp.leases", piholeuser) - actual_rc = host.run(check_leases).rc - assert exit_status_success == actual_rc - check_leases = test_cmd.format("w", "/etc/pihole/dhcp.leases", piholeuser) - actual_rc = host.run(check_leases).rc - # readable install.log - check_install = test_cmd.format("r", "/etc/pihole/install.log", piholeuser) - actual_rc = host.run(check_install).rc - assert exit_status_success == actual_rc - # readable versions - check_localversion = test_cmd.format("r", "/etc/pihole/versions", piholeuser) - actual_rc = host.run(check_localversion).rc - assert exit_status_success == actual_rc - # readable macvendor.db - check_macvendor = test_cmd.format("r", "/etc/pihole/macvendor.db", piholeuser) - actual_rc = host.run(check_macvendor).rc - assert exit_status_success == actual_rc - # check readable and executable /etc/init.d/pihole-FTL - check_init = test_cmd.format("x", "/etc/init.d/pihole-FTL", piholeuser) - actual_rc = host.run(check_init).rc - assert exit_status_success == actual_rc - check_init = test_cmd.format("r", "/etc/init.d/pihole-FTL", piholeuser) - actual_rc = host.run(check_init).rc - assert exit_status_success == actual_rc - # check readable and executable manpages - if maninstalled is True: - check_man = test_cmd.format("x", "/usr/local/share/man", piholeuser) - actual_rc = host.run(check_man).rc - assert exit_status_success == actual_rc - check_man = test_cmd.format("r", "/usr/local/share/man", piholeuser) - actual_rc = host.run(check_man).rc - assert exit_status_success == actual_rc - check_man = test_cmd.format("x", "/usr/local/share/man/man8", piholeuser) - actual_rc = host.run(check_man).rc - assert exit_status_success == actual_rc - check_man = test_cmd.format("r", "/usr/local/share/man/man8", piholeuser) - actual_rc = host.run(check_man).rc - assert exit_status_success == actual_rc - check_man = test_cmd.format( - "r", "/usr/local/share/man/man8/pihole.8", piholeuser - ) - actual_rc = host.run(check_man).rc - assert exit_status_success == actual_rc - # check not readable cron file - check_sudo = test_cmd.format("x", "/etc/cron.d/", piholeuser) - actual_rc = host.run(check_sudo).rc - assert exit_status_success == actual_rc - check_sudo = test_cmd.format("r", "/etc/cron.d/", piholeuser) - actual_rc = host.run(check_sudo).rc - assert exit_status_success == actual_rc - check_sudo = test_cmd.format("r", "/etc/cron.d/pihole", piholeuser) - actual_rc = host.run(check_sudo).rc - assert exit_status_success == actual_rc - directories = get_directories_recursive(host, "/etc/.pihole/") - for directory in directories: - check_pihole = test_cmd.format("r", directory, piholeuser) - actual_rc = host.run(check_pihole).rc - check_pihole = test_cmd.format("x", directory, piholeuser) - actual_rc = host.run(check_pihole).rc - findfiles = 'find "{}" -maxdepth 1 -type f -exec echo {{}} \\;;' - filelist = host.run(findfiles.format(directory)) - files = list(filter(bool, filelist.stdout.splitlines())) - for file in files: - check_pihole = test_cmd.format("r", file, piholeuser) - actual_rc = host.run(check_pihole).rc - - -def test_update_package_cache_success_no_errors(host): - """ - confirms package cache was updated without any errors - """ - updateCache = host.run(""" - source /opt/pihole/basic-install.sh - package_manager_detect - update_package_cache - """) - expected_stdout = tick_box + " Update local cache of available packages" - assert expected_stdout in updateCache.stdout - assert "error" not in updateCache.stdout.lower() - - -def test_update_package_cache_failure_no_errors(host): - """ - confirms package cache was not updated - """ - mock_command("apt-get", {"update": ("", "1")}, host) - updateCache = host.run(""" - source /opt/pihole/basic-install.sh - package_manager_detect - update_package_cache - """) - expected_stdout = cross_box + " Update local cache of available packages" - assert expected_stdout in updateCache.stdout - assert "Error: Unable to update package cache." in updateCache.stdout - - -@pytest.mark.parametrize( - "arch,detected_string,supported", - [ - ("aarch64", "AArch64 (64 Bit ARM)", True), - ("armv6", "ARMv6", True), - ("armv7l", "ARMv7 (or newer)", True), - ("armv7", "ARMv7 (or newer)", True), - ("armv8a", "ARMv7 (or newer)", True), - ("x86_64", "x86_64", True), - ("riscv64", "riscv64", True), - ("mips", "mips", False), - ], -) -def test_FTL_detect_no_errors(host, arch, detected_string, supported): - """ - confirms only correct package is downloaded for FTL engine - """ - # mock uname to return passed platform - mock_command("uname", {"-m": (arch, "0")}, host) - # mock readelf to respond with passed CPU architecture - mock_command_2( - "readelf", - { - "-A /bin/sh": ("Tag_CPU_arch: " + arch, "0"), - "-A /usr/bin/sh": ("Tag_CPU_arch: " + arch, "0"), - "-A /usr/sbin/sh": ("Tag_CPU_arch: " + arch, "0"), - }, - host, - ) - host.run('echo "' + FTL_BRANCH + '" > /etc/pihole/ftlbranch') - detectPlatform = host.run(""" - source /opt/pihole/basic-install.sh - create_pihole_user - funcOutput=$(get_binary_name) - binary="pihole-FTL${funcOutput##*pihole-FTL}" - theRest="${funcOutput%pihole-FTL*}" - FTLdetect "${binary}" "${theRest}" - """) - if supported: - expected_stdout = info_box + " FTL Checks..." - assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + " Detected " + detected_string + " architecture" - assert expected_stdout in detectPlatform.stdout - expected_stdout = tick_box + " Downloading and Installing FTL" - assert expected_stdout in detectPlatform.stdout - else: - expected_stdout = ( - "Not able to detect architecture (unknown: " + detected_string + ")" - ) - assert expected_stdout in detectPlatform.stdout - - -def test_FTL_development_binary_installed_and_responsive_no_errors(host): - """ - confirms FTL development binary is copied and functional in installed location - """ - host.run('echo "' + FTL_BRANCH + '" > /etc/pihole/ftlbranch') - host.run(""" - source /opt/pihole/basic-install.sh - create_pihole_user - funcOutput=$(get_binary_name) - binary="pihole-FTL${funcOutput##*pihole-FTL}" - theRest="${funcOutput%pihole-FTL*}" - FTLdetect "${binary}" "${theRest}" - """) - version_check = host.run(""" - VERSION=$(pihole-FTL version) - echo ${VERSION:0:1} - """) - expected_stdout = "v" - assert expected_stdout in version_check.stdout - - -def test_IPv6_only_link_local(host): - """ - confirms IPv6 blocking is disabled for Link-local address - """ - # mock ip -6 address to return Link-local address - mock_command_2( - "ip", - {"-6 address": ("inet6 fe80::d210:52fa:fe00:7ad7/64 scope link", "0")}, - host, - ) - detectPlatform = host.run(""" - source /opt/pihole/basic-install.sh - find_IPv6_information - """) - expected_stdout = "Unable to find IPv6 ULA/GUA address" - assert expected_stdout in detectPlatform.stdout - - -def test_IPv6_only_ULA(host): - """ - confirms IPv6 blocking is enabled for ULA addresses - """ - # mock ip -6 address to return ULA address - mock_command_2( - "ip", - { - "-6 address": ( - "inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global", - "0", - ) - }, - host, - ) - detectPlatform = host.run(""" - source /opt/pihole/basic-install.sh - find_IPv6_information - """) - expected_stdout = "Found IPv6 ULA address" - assert expected_stdout in detectPlatform.stdout - - -def test_IPv6_only_GUA(host): - """ - confirms IPv6 blocking is enabled for GUA addresses - """ - # mock ip -6 address to return GUA address - mock_command_2( - "ip", - { - "-6 address": ( - "inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global", - "0", - ) - }, - host, - ) - detectPlatform = host.run(""" - source /opt/pihole/basic-install.sh - find_IPv6_information - """) - expected_stdout = "Found IPv6 GUA address" - assert expected_stdout in detectPlatform.stdout - - -def test_IPv6_GUA_ULA_test(host): - """ - confirms IPv6 blocking is enabled for GUA and ULA addresses - """ - # mock ip -6 address to return GUA and ULA addresses - mock_command_2( - "ip", - { - "-6 address": ( - "inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global\n" - "inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global", - "0", - ) - }, - host, - ) - detectPlatform = host.run(""" - source /opt/pihole/basic-install.sh - find_IPv6_information - """) - expected_stdout = "Found IPv6 ULA address" - assert expected_stdout in detectPlatform.stdout - - -def test_IPv6_ULA_GUA_test(host): - """ - confirms IPv6 blocking is enabled for GUA and ULA addresses - """ - # mock ip -6 address to return ULA and GUA addresses - mock_command_2( - "ip", - { - "-6 address": ( - "inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global\n" - "inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global", - "0", - ) - }, - host, - ) - detectPlatform = host.run(""" - source /opt/pihole/basic-install.sh - find_IPv6_information - """) - expected_stdout = "Found IPv6 ULA address" - assert expected_stdout in detectPlatform.stdout - - -def test_validate_ip(host): - """ - Tests valid_ip for various IP addresses - """ - - def test_address(addr, success=True): - output = host.run(""" - source /opt/pihole/basic-install.sh - valid_ip "{addr}" - """.format(addr=addr)) - - assert output.rc == 0 if success else 1 - - test_address("192.168.1.1") - test_address("127.0.0.1") - test_address("255.255.255.255") - test_address("255.255.255.256", False) - test_address("255.255.256.255", False) - test_address("255.256.255.255", False) - test_address("256.255.255.255", False) - test_address("1092.168.1.1", False) - test_address("not an IP", False) - test_address("8.8.8.8#", False) - test_address("8.8.8.8#0") - test_address("8.8.8.8#1") - test_address("8.8.8.8#42") - test_address("8.8.8.8#888") - test_address("8.8.8.8#1337") - test_address("8.8.8.8#65535") - test_address("8.8.8.8#65536", False) - test_address("8.8.8.8#-1", False) - test_address("00.0.0.0", False) - test_address("010.0.0.0", False) - test_address("001.0.0.0", False) - test_address("0.0.0.0#00", False) - test_address("0.0.0.0#01", False) - test_address("0.0.0.0#001", False) - test_address("0.0.0.0#0001", False) - test_address("0.0.0.0#00001", False) - - -def test_package_manager_has_pihole_deps(host): - """Confirms OS is able to install the required packages for Pi-hole""" - mock_command("dialog", {"*": ("", "0")}, host) - output = host.run(""" - source /opt/pihole/basic-install.sh - package_manager_detect - update_package_cache - build_dependency_package - install_dependent_packages - """) - - assert "No package" not in output.stdout - assert output.rc == 0 - - -def test_meta_package_uninstall(host): - """Confirms OS is able to install and uninstall the Pi-hole meta package""" - mock_command("dialog", {"*": ("", "0")}, host) - install = host.run(""" - source /opt/pihole/basic-install.sh - package_manager_detect - update_package_cache - build_dependency_package - install_dependent_packages - """) - assert install.rc == 0 - - uninstall = host.run(""" - source /opt/pihole/uninstall.sh - removeMetaPackage - """) - assert uninstall.rc == 0 diff --git a/test/test_any_utils.py b/test/test_any_utils.py deleted file mode 100644 index 43e637f3..00000000 --- a/test/test_any_utils.py +++ /dev/null @@ -1,49 +0,0 @@ -def test_key_val_replacement_works(host): - """Confirms addOrEditKeyValPair either adds or replaces a key value pair in a given file""" - host.run(""" - source /opt/pihole/utils.sh - addOrEditKeyValPair "./testoutput" "KEY_ONE" "value1" - addOrEditKeyValPair "./testoutput" "KEY_TWO" "value2" - addOrEditKeyValPair "./testoutput" "KEY_ONE" "value3" - addOrEditKeyValPair "./testoutput" "KEY_FOUR" "value4" - """) - output = host.run(""" - cat ./testoutput - """) - expected_stdout = "KEY_ONE=value3\nKEY_TWO=value2\nKEY_FOUR=value4\n" - assert expected_stdout == output.stdout - - -def test_getFTLPID_default(host): - """Confirms getFTLPID returns the default value if FTL is not running""" - output = host.run(""" - source /opt/pihole/utils.sh - getFTLPID - """) - expected_stdout = "-1\n" - assert expected_stdout == output.stdout - - -def test_setFTLConfigValue_getFTLConfigValue(host): - """ - Confirms getFTLConfigValue works (also assumes setFTLConfigValue works) - Requires FTL to be installed, so we do that first - (taken from test_FTL_development_binary_installed_and_responsive_no_errors) - """ - host.run(""" - source /opt/pihole/basic-install.sh - create_pihole_user - funcOutput=$(get_binary_name) - echo "development" > /etc/pihole/ftlbranch - binary="pihole-FTL${funcOutput##*pihole-FTL}" - theRest="${funcOutput%pihole-FTL*}" - FTLdetect "${binary}" "${theRest}" - """) - - output = host.run(""" - source /opt/pihole/utils.sh - setFTLConfigValue "dns.upstreams" '["9.9.9.9"]' > /dev/null - getFTLConfigValue "dns.upstreams" - """) - - assert "[ 9.9.9.9 ]" in output.stdout diff --git a/test/test_automated_install.bats b/test/test_automated_install.bats new file mode 100755 index 00000000..5fb2cd3c --- /dev/null +++ b/test/test_automated_install.bats @@ -0,0 +1,371 @@ +#!/usr/bin/env bats +# Tests for basic-install.sh — translated from test_any_automated_install.py + +load 'libs/bats-support/load' +load 'libs/bats-assert/load' +load 'helpers/mocks' + +TICK="[✓]" +CROSS="[✗]" +INFO="[i]" + +FTL_BRANCH="development" + +CID="" + +setup() { + CID=$(docker run -d -t --cap-add=ALL "$IMAGE_TAG") +} + +teardown() { + if [[ -n "$CID" ]]; then + docker rm -f "$CID" > /dev/null 2>&1 || true + fi +} + +# --------------------------------------------------------------------------- + +@test "installer exits when no supported package manager found" { + docker exec "$CID" bash -c "rm -rf /usr/bin/apt-get /usr/bin/rpm /sbin/apk" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + package_manager_detect + " + assert_output --partial "${CROSS} No supported package manager found" +} + +@test "installer continues when SELinux config file does not exist" { + run docker exec "$CID" bash -c " + rm -f /etc/selinux/config + source /opt/pihole/basic-install.sh + checkSelinux + " + assert_output --partial "${INFO} SELinux not detected" + assert_success +} + +@test "fresh install: all necessary files are readable by pihole user" { + mock_command "$CID" dialog "*" "" "0" + mock_command_passthrough "$CID" git "pull" "" "0" + mock_command_2 "$CID" systemctl \ + "enable pihole-FTL" "" "0" \ + "restart pihole-FTL" "" "0" \ + "start pihole-FTL" "" "0" + mock_command_2 "$CID" rc-service \ + "pihole-FTL enable" "" "0" \ + "pihole-FTL restart" "" "0" \ + "pihole-FTL start" "" "0" + + # Install man pages if available (best-effort) + docker exec "$CID" bash -c "command -v apt-get > /dev/null && apt-get install -qq man" || true + docker exec "$CID" bash -c "command -v dnf > /dev/null && dnf install -y man" || true + docker exec "$CID" bash -c "command -v yum > /dev/null && yum install -y man" || true + docker exec "$CID" bash -c "command -v apk > /dev/null && apk add mandoc man-pages" || true + + docker exec "$CID" bash -c "echo '${FTL_BRANCH}' > /etc/pihole/ftlbranch" + + run docker exec "$CID" bash -c " + export TERM=xterm + export DEBIAN_FRONTEND=noninteractive + umask 0027 + runUnattended=true + source /opt/pihole/basic-install.sh > /dev/null + runUnattended=true + main + /opt/pihole/pihole-FTL-prestart.sh + " + assert_success + + # Detect whether man was installed + local maninstalled=true + if [[ "$output" == *"${INFO} man not installed"* ]] || [[ "$output" == *"${INFO} man pages not installed"* ]]; then + maninstalled=false + fi + + local piholeuser="pihole" + _check_perm() { docker exec "$CID" bash -c "su -s /bin/bash -c 'test -${1} ${2}' -p ${piholeuser}"; } + + # /etc/pihole + run _check_perm r /etc/pihole; assert_success + run _check_perm x /etc/pihole; assert_success + + # /etc/pihole/dhcp.leases + run _check_perm r /etc/pihole/dhcp.leases; assert_success + + # /etc/pihole/install.log + run _check_perm r /etc/pihole/install.log; assert_success + + # /etc/pihole/versions + run _check_perm r /etc/pihole/versions; assert_success + + # /etc/pihole/macvendor.db + run _check_perm r /etc/pihole/macvendor.db; assert_success + + # /etc/init.d/pihole-FTL + run _check_perm x /etc/init.d/pihole-FTL; assert_success + run _check_perm r /etc/init.d/pihole-FTL; assert_success + + # man pages (if installed) + if [[ "$maninstalled" == "true" ]]; then + run _check_perm x /usr/local/share/man; assert_success + run _check_perm r /usr/local/share/man; assert_success + run _check_perm x /usr/local/share/man/man8; assert_success + run _check_perm r /usr/local/share/man/man8; assert_success + run _check_perm r /usr/local/share/man/man8/pihole.8; assert_success + fi + + # /etc/cron.d + run _check_perm x /etc/cron.d/; assert_success + run _check_perm r /etc/cron.d/; assert_success + run _check_perm r /etc/cron.d/pihole; assert_success + + # All files and directories under /etc/.pihole/ + local dirs + dirs=$(docker exec "$CID" bash -c "find /etc/.pihole/ -type d -not -path '*/.*'" 2>/dev/null || true) + while IFS= read -r dir; do + [[ -z "$dir" ]] && continue + run _check_perm r "$dir"; assert_success + run _check_perm x "$dir"; assert_success + local files + files=$(docker exec "$CID" bash -c "find '${dir}' -maxdepth 1 -type f -exec echo {} \\;" 2>/dev/null || true) + while IFS= read -r file; do + [[ -z "$file" ]] && continue + run _check_perm r "$file"; assert_success + done <<< "$files" + done <<< "$dirs" +} + +@test "package cache update succeeds without errors" { + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + package_manager_detect + update_package_cache + " + assert_output --partial "${TICK} Update local cache of available packages" + refute_output --partial "error" +} + +@test "package cache update reports failure correctly" { + mock_command "$CID" apt-get "update" "" "1" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + package_manager_detect + update_package_cache + " + assert_output --partial "${CROSS} Update local cache of available packages" + assert_output --partial "Error: Unable to update package cache." +} + +# --------------------------------------------------------------------------- +# FTL architecture detection — one @test per arch (replaces parametrize) +# --------------------------------------------------------------------------- + +_test_ftl_arch() { + local arch="$1" detected_string="$2" supported="$3" + + mock_command "$CID" uname "-m" "$arch" "0" + mock_command_2 "$CID" readelf \ + "-A /bin/sh" "Tag_CPU_arch: ${arch}" "0" \ + "-A /usr/bin/sh" "Tag_CPU_arch: ${arch}" "0" \ + "-A /usr/sbin/sh" "Tag_CPU_arch: ${arch}" "0" + docker exec "$CID" bash -c "echo '${FTL_BRANCH}' > /etc/pihole/ftlbranch" + + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + create_pihole_user + funcOutput=\$(get_binary_name) + binary=\"pihole-FTL\${funcOutput##*pihole-FTL}\" + theRest=\"\${funcOutput%pihole-FTL*}\" + FTLdetect \"\${binary}\" \"\${theRest}\" + " + + if [[ "$supported" == "true" ]]; then + assert_output --partial "${INFO} FTL Checks..." + assert_output --partial "${TICK} Detected ${detected_string} architecture" + assert_output --partial "${TICK} Downloading and Installing FTL" + else + assert_output --partial "Not able to detect architecture (unknown: ${detected_string})" + fi +} + +@test "FTL detects aarch64 architecture" { + _test_ftl_arch "aarch64" "AArch64 (64 Bit ARM)" "true" +} + +@test "FTL detects ARMv6 architecture" { + _test_ftl_arch "armv6" "ARMv6" "true" +} + +@test "FTL detects ARMv7l architecture" { + _test_ftl_arch "armv7l" "ARMv7 (or newer)" "true" +} + +@test "FTL detects ARMv7 architecture" { + _test_ftl_arch "armv7" "ARMv7 (or newer)" "true" +} + +@test "FTL detects ARMv8a architecture" { + _test_ftl_arch "armv8a" "ARMv7 (or newer)" "true" +} + +@test "FTL detects x86_64 architecture" { + _test_ftl_arch "x86_64" "x86_64" "true" +} + +@test "FTL detects riscv64 architecture" { + _test_ftl_arch "riscv64" "riscv64" "true" +} + +@test "FTL reports unsupported architecture" { + _test_ftl_arch "mips" "mips" "false" +} + +@test "FTL development binary is installed and responsive" { + docker exec "$CID" bash -c "echo '${FTL_BRANCH}' > /etc/pihole/ftlbranch" + docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + create_pihole_user + funcOutput=\$(get_binary_name) + binary=\"pihole-FTL\${funcOutput##*pihole-FTL}\" + theRest=\"\${funcOutput%pihole-FTL*}\" + FTLdetect \"\${binary}\" \"\${theRest}\" + " + run docker exec "$CID" bash -c ' + VERSION=$(pihole-FTL version) + echo "${VERSION:0:1}" + ' + assert_output --partial "v" +} + +# --------------------------------------------------------------------------- +# IPv6 detection +# --------------------------------------------------------------------------- + +@test "IPv6 link-local only: blocking disabled" { + mock_command_2 "$CID" ip \ + "-6 address" "inet6 fe80::d210:52fa:fe00:7ad7/64 scope link" "0" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + find_IPv6_information + " + assert_output --partial "Unable to find IPv6 ULA/GUA address" +} + +@test "IPv6 ULA only: blocking enabled" { + mock_command_2 "$CID" ip \ + "-6 address" "inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global" "0" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + find_IPv6_information + " + assert_output --partial "Found IPv6 ULA address" +} + +@test "IPv6 GUA only: blocking enabled" { + mock_command_2 "$CID" ip \ + "-6 address" "inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global" "0" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + find_IPv6_information + " + assert_output --partial "Found IPv6 GUA address" +} + +@test "IPv6 GUA + ULA: ULA takes precedence" { + mock_command_2 "$CID" ip \ + "-6 address" "inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global +inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global" "0" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + find_IPv6_information + " + assert_output --partial "Found IPv6 ULA address" +} + +@test "IPv6 ULA + GUA: ULA takes precedence" { + mock_command_2 "$CID" ip \ + "-6 address" "inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global +inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global" "0" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + find_IPv6_information + " + assert_output --partial "Found IPv6 ULA address" +} + +# --------------------------------------------------------------------------- +# IP address validation +# --------------------------------------------------------------------------- + +@test "valid_ip accepts and rejects addresses correctly" { + _valid() { + run docker exec "$CID" bash -c "source /opt/pihole/basic-install.sh; valid_ip '${1}'" + assert_success + } + _invalid() { + run docker exec "$CID" bash -c "source /opt/pihole/basic-install.sh; valid_ip '${1}'" + assert_failure + } + + _valid "192.168.1.1" + _valid "127.0.0.1" + _valid "255.255.255.255" + _invalid "255.255.255.256" + _invalid "255.255.256.255" + _invalid "255.256.255.255" + _invalid "256.255.255.255" + _invalid "1092.168.1.1" + _invalid "not an IP" + _invalid "8.8.8.8#" + _valid "8.8.8.8#0" + _valid "8.8.8.8#1" + _valid "8.8.8.8#42" + _valid "8.8.8.8#888" + _valid "8.8.8.8#1337" + _valid "8.8.8.8#65535" + _invalid "8.8.8.8#65536" + _invalid "8.8.8.8#-1" + _invalid "00.0.0.0" + _invalid "010.0.0.0" + _invalid "001.0.0.0" + _invalid "0.0.0.0#00" + _invalid "0.0.0.0#01" + _invalid "0.0.0.0#001" + _invalid "0.0.0.0#0001" + _invalid "0.0.0.0#00001" +} + +# --------------------------------------------------------------------------- +# Package dependency installation +# --------------------------------------------------------------------------- + +@test "OS can install required Pi-hole dependency packages" { + mock_command "$CID" dialog "*" "" "0" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + package_manager_detect + update_package_cache + build_dependency_package + install_dependent_packages + " + refute_output --partial "No package" + assert_success +} + +@test "OS can install and uninstall the Pi-hole meta package" { + mock_command "$CID" dialog "*" "" "0" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + package_manager_detect + update_package_cache + build_dependency_package + install_dependent_packages + " + assert_success + + run docker exec "$CID" bash -c " + source /opt/pihole/uninstall.sh + removeMetaPackage + " + assert_success +} diff --git a/test/test_centos_fedora_common_support.py b/test/test_centos_fedora_common_support.py deleted file mode 100644 index a892db87..00000000 --- a/test/test_centos_fedora_common_support.py +++ /dev/null @@ -1,65 +0,0 @@ -from .conftest import ( - tick_box, - cross_box, - mock_command, -) - - -def mock_selinux_config(state, host): - """ - Creates a mock SELinux config file with expected content - """ - # validate state string - valid_states = ["enforcing", "permissive", "disabled"] - assert state in valid_states - # getenforce returns the running state of SELinux - mock_command("getenforce", {"*": (state.capitalize(), "0")}, host) - # create mock configuration with desired content - host.run(""" - mkdir /etc/selinux - echo "SELINUX={state}" > /etc/selinux/config - """.format(state=state.lower())) - - -def test_selinux_enforcing_exit(host): - """ - confirms installer prompts to exit when SELinux is Enforcing by default - """ - mock_selinux_config("enforcing", host) - check_selinux = host.run(""" - source /opt/pihole/basic-install.sh - checkSelinux - """) - expected_stdout = cross_box + " Current SELinux: enforcing" - assert expected_stdout in check_selinux.stdout - expected_stdout = "SELinux Enforcing detected, exiting installer" - assert expected_stdout in check_selinux.stdout - assert check_selinux.rc == 1 - - -def test_selinux_permissive(host): - """ - confirms installer continues when SELinux is Permissive - """ - mock_selinux_config("permissive", host) - check_selinux = host.run(""" - source /opt/pihole/basic-install.sh - checkSelinux - """) - expected_stdout = tick_box + " Current SELinux: permissive" - assert expected_stdout in check_selinux.stdout - assert check_selinux.rc == 0 - - -def test_selinux_disabled(host): - """ - confirms installer continues when SELinux is Disabled - """ - mock_selinux_config("disabled", host) - check_selinux = host.run(""" - source /opt/pihole/basic-install.sh - checkSelinux - """) - expected_stdout = tick_box + " Current SELinux: disabled" - assert expected_stdout in check_selinux.stdout - assert check_selinux.rc == 0 diff --git a/test/test_selinux.bats b/test/test_selinux.bats new file mode 100755 index 00000000..08d205d1 --- /dev/null +++ b/test/test_selinux.bats @@ -0,0 +1,71 @@ +#!/usr/bin/env bats +# Tests for SELinux handling in basic-install.sh. +# Translated from test_centos_fedora_common_support.py. +# Only runs on rhel family (CentOS/Fedora) — selected by run.sh. + +load 'libs/bats-support/load' +load 'libs/bats-assert/load' +load 'helpers/mocks' + +TICK="[✓]" +CROSS="[✗]" + +CID="" + +setup() { + CID=$(docker run -d -t --cap-add=ALL "$IMAGE_TAG") +} + +teardown() { + if [[ -n "$CID" ]]; then + docker rm -f "$CID" > /dev/null 2>&1 || true + fi +} + +# --------------------------------------------------------------------------- +# Helper: write a mock SELinux config with the given state +# --------------------------------------------------------------------------- + +_mock_selinux_config() { + local state="$1" # enforcing, permissive, or disabled + local capitalized + capitalized=$(echo "${state}" | awk '{print toupper(substr($0,1,1)) substr($0,2)}') + mock_command "$CID" getenforce "*" "$capitalized" "0" + docker exec "$CID" bash -c " + mkdir -p /etc/selinux + echo 'SELINUX=${state}' > /etc/selinux/config + " +} + +# --------------------------------------------------------------------------- + +@test "SELinux enforcing: installer exits with error" { + _mock_selinux_config "enforcing" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + checkSelinux + " + assert_output --partial "${CROSS} Current SELinux: enforcing" + assert_output --partial "SELinux Enforcing detected, exiting installer" + assert_failure +} + +@test "SELinux permissive: installer continues" { + _mock_selinux_config "permissive" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + checkSelinux + " + assert_output --partial "${TICK} Current SELinux: permissive" + assert_success +} + +@test "SELinux disabled: installer continues" { + _mock_selinux_config "disabled" + run docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + checkSelinux + " + assert_output --partial "${TICK} Current SELinux: disabled" + assert_success +} diff --git a/test/test_utils.bats b/test/test_utils.bats new file mode 100755 index 00000000..1b0098c0 --- /dev/null +++ b/test/test_utils.bats @@ -0,0 +1,60 @@ +#!/usr/bin/env bats +# Tests for utils.sh — translated from test_any_utils.py + +load 'libs/bats-support/load' +load 'libs/bats-assert/load' + +CID="" + +setup() { + CID=$(docker run -d -t --cap-add=ALL "$IMAGE_TAG") +} + +teardown() { + if [[ -n "$CID" ]]; then + docker rm -f "$CID" > /dev/null 2>&1 || true + fi +} + +# --------------------------------------------------------------------------- + +@test "addOrEditKeyValPair adds and replaces key-value pairs correctly" { + docker exec "$CID" bash -c " + source /opt/pihole/utils.sh + addOrEditKeyValPair './testoutput' 'KEY_ONE' 'value1' + addOrEditKeyValPair './testoutput' 'KEY_TWO' 'value2' + addOrEditKeyValPair './testoutput' 'KEY_ONE' 'value3' + addOrEditKeyValPair './testoutput' 'KEY_FOUR' 'value4' + " + run docker exec "$CID" bash -c "cat ./testoutput" + assert_output "KEY_ONE=value3 +KEY_TWO=value2 +KEY_FOUR=value4" +} + +@test "getFTLPID returns -1 when FTL is not running" { + run docker exec "$CID" bash -c " + source /opt/pihole/utils.sh + getFTLPID + " + assert_output "-1" +} + +@test "setFTLConfigValue and getFTLConfigValue round-trip" { + # FTL must be installed for this test + docker exec "$CID" bash -c " + source /opt/pihole/basic-install.sh + create_pihole_user + funcOutput=\$(get_binary_name) + echo 'development' > /etc/pihole/ftlbranch + binary=\"pihole-FTL\${funcOutput##*pihole-FTL}\" + theRest=\"\${funcOutput%pihole-FTL*}\" + FTLdetect \"\${binary}\" \"\${theRest}\" + " + run docker exec "$CID" bash -c " + source /opt/pihole/utils.sh + setFTLConfigValue 'dns.upstreams' '[\"9.9.9.9\"]' > /dev/null + getFTLConfigValue 'dns.upstreams' + " + assert_output --partial "[ 9.9.9.9 ]" +} diff --git a/test/tox.alpine_3_21.ini b/test/tox.alpine_3_21.ini deleted file mode 100644 index b0465f6c..00000000 --- a/test/tox.alpine_3_21.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv:py3] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _alpine_3_21.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.alpine_3_22.ini b/test/tox.alpine_3_22.ini deleted file mode 100644 index 38f66c4f..00000000 --- a/test/tox.alpine_3_22.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv:py3] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _alpine_3_22.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.alpine_3_23.ini b/test/tox.alpine_3_23.ini deleted file mode 100644 index d7208064..00000000 --- a/test/tox.alpine_3_23.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv:py3] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _alpine_3_23.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.centos_10.ini b/test/tox.centos_10.ini deleted file mode 100644 index 1a15c766..00000000 --- a/test/tox.centos_10.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv:py3] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _centos_10.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py diff --git a/test/tox.centos_9.ini b/test/tox.centos_9.ini deleted file mode 100644 index 81dd0bd2..00000000 --- a/test/tox.centos_9.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv:py3] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _centos_9.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py diff --git a/test/tox.debian_11.ini b/test/tox.debian_11.ini deleted file mode 100644 index a8909d46..00000000 --- a/test/tox.debian_11.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv:py3] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _debian_11.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.debian_12.ini b/test/tox.debian_12.ini deleted file mode 100644 index 707e8710..00000000 --- a/test/tox.debian_12.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv:py3] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _debian_12.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.debian_13.ini b/test/tox.debian_13.ini deleted file mode 100644 index dcfbf816..00000000 --- a/test/tox.debian_13.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv:py3] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _debian_13.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.fedora_40.ini b/test/tox.fedora_40.ini deleted file mode 100644 index 462c5ff1..00000000 --- a/test/tox.fedora_40.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _fedora_40.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py diff --git a/test/tox.fedora_41.ini b/test/tox.fedora_41.ini deleted file mode 100644 index f70da227..00000000 --- a/test/tox.fedora_41.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _fedora_41.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py diff --git a/test/tox.fedora_42.ini b/test/tox.fedora_42.ini deleted file mode 100644 index 67eb77e4..00000000 --- a/test/tox.fedora_42.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _fedora_42.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py diff --git a/test/tox.fedora_43.ini b/test/tox.fedora_43.ini deleted file mode 100644 index efbb0471..00000000 --- a/test/tox.fedora_43.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _fedora_43.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py ./test_centos_fedora_common_support.py diff --git a/test/tox.ubuntu_20.ini b/test/tox.ubuntu_20.ini deleted file mode 100644 index bcfb1d2a..00000000 --- a/test/tox.ubuntu_20.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv:py3] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _ubuntu_20.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.ubuntu_22.ini b/test/tox.ubuntu_22.ini deleted file mode 100644 index c8e71abb..00000000 --- a/test/tox.ubuntu_22.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv:py3] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _ubuntu_22.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py diff --git a/test/tox.ubuntu_24.ini b/test/tox.ubuntu_24.ini deleted file mode 100644 index 5b7e77a9..00000000 --- a/test/tox.ubuntu_24.ini +++ /dev/null @@ -1,10 +0,0 @@ -[tox] -envlist = py3 - -[testenv:py3] -allowlist_externals = docker -deps = -rrequirements.txt -setenv = - COLUMNS=120 -commands = docker buildx build --load --progress plain -f _ubuntu_24.Dockerfile -t pytest_pihole:test_container ../ - pytest {posargs:-vv -n auto} ./test_any_automated_install.py ./test_any_utils.py