From 6e94450826de97181407aa612527dbd100d605f6 Mon Sep 17 00:00:00 2001 From: Adam Warner Date: Sat, 25 Apr 2026 00:11:54 +0100 Subject: [PATCH 01/14] Only check for curl/local file retrieval if download == true, prevents double error messages in certain situations Signed-off-by: Adam Warner --- gravity.sh | 81 +++++++++++++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/gravity.sh b/gravity.sh index 71f428fb..7ea3a183 100755 --- a/gravity.sh +++ b/gravity.sh @@ -796,48 +796,49 @@ gravity_DownloadBlocklistFromUrl() { # a generic message is returned. curlOutput=$(curl --connect-timeout ${curl_connect_timeout} -s --fail -L ${compression:+${compression}} ${customUpstreamResolver:+${customUpstreamResolver}} "${modifiedOptions[@]}" -w "${curlOutputFormat}" "${url}" -o "${listCurlBuffer}") curlExitCode="$?" + + + # Retrieve http_code and errormsg values, returned by curl command + IFS=";" read -r httpCode curlErrorMsg <<<"$curlOutput" + + case $url in + # Did we "download" a local file? + "file"*) + if [[ -s "${listCurlBuffer}" ]]; then + echo -e "${OVER} ${TICK} ${str} Retrieval successful" + success=true + else + echo -e "${OVER} ${CROSS} ${str} Retrieval failed / empty list" + fi + ;; + # Did we "download" a remote file? + *) + # Use the exit code to determine if curl was successful or not. + # Use HTTP code only to select the correct error message. + if [[ "${curlExitCode}" == "0" ]]; then + case "${httpCode}" in + "200") echo -e "${OVER} ${TICK} ${str} Retrieval successful" ;; + "304") echo -e "${OVER} ${TICK} ${str} No changes detected" ;; + *) echo -e "${OVER} ${TICK} ${str} Success (http_code=${COL_CYAN}${httpCode}${COL_NC})" ;; + esac + success=true + else + case "${httpCode}" in + "403") echo -e "${OVER} ${CROSS} ${str} Forbidden" ;; + "404") echo -e "${OVER} ${CROSS} ${str} Not found" ;; + "408") echo -e "${OVER} ${CROSS} ${str} Time-out" ;; + "451") echo -e "${OVER} ${CROSS} ${str} Unavailable For Legal Reasons" ;; + "500") echo -e "${OVER} ${CROSS} ${str} Internal Server Error" ;; + "504") echo -e "${OVER} ${CROSS} ${str} Connection Timed Out (Gateway)" ;; + "521") echo -e "${OVER} ${CROSS} ${str} Web Server Is Down (Cloudflare)" ;; + "522") echo -e "${OVER} ${CROSS} ${str} Connection Timed Out (Cloudflare)" ;; + *) echo -e "${OVER} ${CROSS} ${str} Retrieval failed (exit_code=${COL_CYAN}${curlExitCode}${COL_NC} Msg: ${COL_CYAN}${curlErrorMsg}${COL_NC})" ;; + esac + fi + ;; + esac fi - # Retrieve http_code and errormsg values, returned by curl command - IFS=";" read -r httpCode curlErrorMsg <<<"$curlOutput" - - case $url in - # Did we "download" a local file? - "file"*) - if [[ -s "${listCurlBuffer}" ]]; then - echo -e "${OVER} ${TICK} ${str} Retrieval successful" - success=true - else - echo -e "${OVER} ${CROSS} ${str} Retrieval failed / empty list" - fi - ;; - # Did we "download" a remote file? - *) - # Use the exit code to determine if curl was successful or not. - # Use HTTP code only to select the correct error message. - if [[ "${curlExitCode}" == "0" ]]; then - case "${httpCode}" in - "200") echo -e "${OVER} ${TICK} ${str} Retrieval successful" ;; - "304") echo -e "${OVER} ${TICK} ${str} No changes detected" ;; - *) echo -e "${OVER} ${TICK} ${str} Success (http_code=${COL_CYAN}${httpCode}${COL_NC})" ;; - esac - success=true - else - case "${httpCode}" in - "403") echo -e "${OVER} ${CROSS} ${str} Forbidden" ;; - "404") echo -e "${OVER} ${CROSS} ${str} Not found" ;; - "408") echo -e "${OVER} ${CROSS} ${str} Time-out" ;; - "451") echo -e "${OVER} ${CROSS} ${str} Unavailable For Legal Reasons" ;; - "500") echo -e "${OVER} ${CROSS} ${str} Internal Server Error" ;; - "504") echo -e "${OVER} ${CROSS} ${str} Connection Timed Out (Gateway)" ;; - "521") echo -e "${OVER} ${CROSS} ${str} Web Server Is Down (Cloudflare)" ;; - "522") echo -e "${OVER} ${CROSS} ${str} Connection Timed Out (Cloudflare)" ;; - *) echo -e "${OVER} ${CROSS} ${str} Retrieval failed (exit_code=${COL_CYAN}${curlExitCode}${COL_NC} Msg: ${COL_CYAN}${curlErrorMsg}${COL_NC})" ;; - esac - fi - ;; - esac - local done="false" # Determine if the blocklist was downloaded and saved correctly if [[ "${success}" == true ]]; then From 4360ac403d3cf3b31a0eba25c3222ccf7351c32f Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Sun, 26 Apr 2026 18:25:30 -0300 Subject: [PATCH 02/14] Replace `head` with `sed` to avoid error messages on the web interface Signed-off-by: RD WebDesign --- gravity.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/gravity.sh b/gravity.sh index 71f428fb..bcd93d01 100755 --- a/gravity.sh +++ b/gravity.sh @@ -782,8 +782,9 @@ gravity_DownloadBlocklistFromUrl() { # Compatibility notes: # Busybox doesn't support some long flags: # - "sort -V" is short form of "sort --version-sort" - # - "head -n1" is short form of "head --lines=1" - if [[ "$(printf '%s\n' "${curlVersion}" "7.75" | sort -V | head -n1)" == 7.75 ]]; then + # Note: "sed '1q'" returns only the first line (like "head -n1"), but it doesn't generate an error message. + # (see https://github.com/pi-hole/pi-hole/issues/6615) + if [[ "$(printf '%s\n' "${curlVersion}" "7.75" | sort -V | sed '1q')" == 7.75 ]]; then # Use the error message returned by curl curlOutputFormat='%{http_code};%{errormsg}' fi From 7e72e17304487aad0b4465637718da98f075d5f7 Mon Sep 17 00:00:00 2001 From: RD WebDesign Date: Mon, 27 Apr 2026 16:55:26 -0300 Subject: [PATCH 03/14] Use `awk` to compare curl versions and edit the comments to explain the new commands. Signed-off-by: RD WebDesign --- gravity.sh | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/gravity.sh b/gravity.sh index bcd93d01..5ec749af 100755 --- a/gravity.sh +++ b/gravity.sh @@ -772,19 +772,22 @@ gravity_DownloadBlocklistFromUrl() { # Define the generic error message curlOutputFormat='%{http_code};No message available. Non supported curl version.' - # Check if the installed curl version supports the "-w %{errormsg}" option (available as of curl 7.75.0) - # (https://github.com/pi-hole/pi-hole/pull/6605#discussion_r3112153347) - # First we get the current curl version. + # Get the current installed curl version. curlVersion=$(curl --version | awk '{print $2;exit}') - # After that, we pipe the current version along with the string '7.75' (the minimum version supporting the required option.) - # Then we sort the list in natural (version) order and return the first item which will be the lowest version seen. - # If it is "7.75" then the current version is greater than or equal to "7.75.0". - # Compatibility notes: - # Busybox doesn't support some long flags: - # - "sort -V" is short form of "sort --version-sort" - # Note: "sed '1q'" returns only the first line (like "head -n1"), but it doesn't generate an error message. + + # Check if the installed curl version supports the "-w %{errormsg}" option. + # The minimum curl version supporting this option is 7.75.0. + # (https://github.com/pi-hole/pi-hole/pull/6605#discussion_r3112153347) + # + # We use "awk" to compare versions by subtracting 7.75 from the version number. + # If the result is greater than or equal to zero, the option is supported. # (see https://github.com/pi-hole/pi-hole/issues/6615) - if [[ "$(printf '%s\n' "${curlVersion}" "7.75" | sort -V | sed '1q')" == 7.75 ]]; then + # + # Notes: + # - Use parameter expansion to get only Major and Minor version parts (containing only one dot). + # - The comparison result will be true or false. We use it as exit code. + # - awk considers "true=1". We negate the comparison to exit with "0" when a desired version is found. + if echo "${curlVersion%.*}" | awk '{exit !($1 - 7.75 >= 0)}'; then # Use the error message returned by curl curlOutputFormat='%{http_code};%{errormsg}' fi From 9beb6081479afa121df257ed4d2e30c194b63709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 28 Apr 2026 21:12:55 +0200 Subject: [PATCH 04/14] Explicitly add `gawk` to APK dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- automated install/basic-install.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/automated install/basic-install.sh b/automated install/basic-install.sh index 73deb53b..eaee8747 100755 --- a/automated install/basic-install.sh +++ b/automated install/basic-install.sh @@ -162,7 +162,7 @@ EOM ) # List of required packages on APK based systems -PIHOLE_META_VERSION_APK=0.2 +PIHOLE_META_VERSION_APK=0.3 PIHOLE_META_DEPS_APK=( bash bash-completion @@ -172,6 +172,7 @@ PIHOLE_META_DEPS_APK=( cronie curl dialog + gawk git grep iproute2-minimal # piholeARPTable.sh From f7d4429d9dbc2346a52c8f939948bce5101dd22d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 10:02:56 +0000 Subject: [PATCH 05/14] Bump github/codeql-action in the github-actions-dependencies group Bumps the github-actions-dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 4.35.2 to 4.35.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/95e58e9a2cdfd71adc6e0353d5c52f41a045d225...e46ed2cbd01164d986452f91f178727624ae40d7) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 099a8788..656d8ad9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,12 +29,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 #v4.35.2 + uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 #v4.35.3 with: languages: 'python' - name: Autobuild - uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 #v4.35.2 + uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 #v4.35.3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 #v4.35.2 + uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 #v4.35.3 From ff8e6357ee11ff609b99ecc1f538896074abcc53 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 10:02:55 +0000 Subject: [PATCH 06/14] Bump github/codeql-action in the github-actions-dependencies group Bumps the github-actions-dependencies group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 4.35.3 to 4.35.4 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/e46ed2cbd01164d986452f91f178727624ae40d7...68bde559dea0fdcac2102bfdf6230c5f70eb485e) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 656d8ad9..812841e8 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,12 +29,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 #v4.35.3 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e #v4.35.4 with: languages: 'python' - name: Autobuild - uses: github/codeql-action/autobuild@e46ed2cbd01164d986452f91f178727624ae40d7 #v4.35.3 + uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e #v4.35.4 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 #v4.35.3 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e #v4.35.4 From 08d44b7a012caa23139d13f6a415bdc110399831 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 May 2026 10:02:59 +0000 Subject: [PATCH 07/14] Bump the github-actions-dependencies group with 2 updates Bumps the github-actions-dependencies group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [trufflesecurity/trufflehog](https://github.com/trufflesecurity/trufflehog). Updates `github/codeql-action` from 4.35.4 to 4.35.5 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/68bde559dea0fdcac2102bfdf6230c5f70eb485e...9e0d7b8d25671d64c341c19c0152d693099fb5ba) Updates `trufflesecurity/trufflehog` from 3.95.2 to 3.95.3 - [Release notes](https://github.com/trufflesecurity/trufflehog/releases) - [Commits](https://github.com/trufflesecurity/trufflehog/compare/17456f8c7d042d8c82c9a8ca9e937231f9f42e26...37b77001d0174ebec2fcca2bd83ff83a6d45a3ab) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.5 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-dependencies - dependency-name: trufflesecurity/trufflehog dependency-version: 3.95.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions-dependencies ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- .github/workflows/test.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 812841e8..aebc4baf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -29,12 +29,12 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e #v4.35.4 + uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba #v4.35.5 with: languages: 'python' - name: Autobuild - uses: github/codeql-action/autobuild@68bde559dea0fdcac2102bfdf6230c5f70eb485e #v4.35.4 + uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba #v4.35.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e #v4.35.4 + uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba #v4.35.5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b1bb766..0519ecfc 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ jobs: display-engine: sarif-fmt - name: Secret Scanning with TruffleHog - uses: trufflesecurity/trufflehog@17456f8c7d042d8c82c9a8ca9e937231f9f42e26 #v3.95.2 + uses: trufflesecurity/trufflehog@37b77001d0174ebec2fcca2bd83ff83a6d45a3ab #v3.95.3 with: extra_args: --results=verified,unknown From da333b09821a173ae6b4dfabf054db608e2ad75e Mon Sep 17 00:00:00 2001 From: Adam Warner Date: Mon, 13 Apr 2026 21:54:24 +0100 Subject: [PATCH 08/14] test: replace pytest/tox with direct in-container BATS Replace the Python/tox-based test stack with BATS and run tests directly inside each built DISTRO container. Remove docker:cli orchestration and unify local mock helpers in test/helpers/mocks.bash for direct execution. Signed-off-by: Adam Warner --- .github/workflows/codeql-analysis.yml | 40 -- .github/workflows/test.yml | 30 +- .gitignore | 10 +- test/__init__.py | 0 test/conftest.py | 175 -------- test/helpers/mocks.bash | 102 +++++ test/requirements.txt | 6 - test/run.sh | 143 +++++++ test/setup.py | 7 - test/test_any_automated_install.py | 472 ---------------------- test/test_any_utils.py | 50 --- test/test_automated_install.bats | 187 +++++++++ test/test_centos_fedora_common_support.py | 65 --- test/test_ftl.bats | 105 +++++ test/test_network.bats | 112 +++++ test/test_selinux.bats | 58 +++ test/test_utils.bats | 52 +++ test/tox.alpine_3_21.ini | 10 - test/tox.alpine_3_22.ini | 10 - test/tox.alpine_3_23.ini | 10 - test/tox.centos_10.ini | 10 - test/tox.centos_9.ini | 10 - test/tox.debian_11.ini | 10 - test/tox.debian_12.ini | 10 - test/tox.debian_13.ini | 10 - test/tox.fedora_40.ini | 10 - test/tox.fedora_41.ini | 10 - test/tox.fedora_42.ini | 10 - test/tox.fedora_43.ini | 10 - test/tox.ubuntu_20.ini | 10 - test/tox.ubuntu_22.ini | 10 - test/tox.ubuntu_24.ini | 10 - 32 files changed, 763 insertions(+), 1001 deletions(-) delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 test/__init__.py delete mode 100644 test/conftest.py create mode 100755 test/helpers/mocks.bash delete mode 100644 test/requirements.txt create mode 100755 test/run.sh delete mode 100644 test/setup.py delete mode 100644 test/test_any_automated_install.py delete mode 100644 test/test_any_utils.py create mode 100755 test/test_automated_install.bats delete mode 100644 test/test_centos_fedora_common_support.py create mode 100755 test/test_ftl.bats create mode 100755 test/test_network.bats create mode 100755 test/test_selinux.bats create mode 100755 test/test_utils.bats delete mode 100644 test/tox.alpine_3_21.ini delete mode 100644 test/tox.alpine_3_22.ini delete mode 100644 test/tox.alpine_3_23.ini delete mode 100644 test/tox.centos_10.ini delete mode 100644 test/tox.centos_9.ini delete mode 100644 test/tox.debian_11.ini delete mode 100644 test/tox.debian_12.ini delete mode 100644 test/tox.debian_13.ini delete mode 100644 test/tox.fedora_40.ini delete mode 100644 test/tox.fedora_41.ini delete mode 100644 test/tox.fedora_42.ini delete mode 100644 test/tox.fedora_43.ini delete mode 100644 test/tox.ubuntu_20.ini delete mode 100644 test/tox.ubuntu_22.ini delete mode 100644 test/tox.ubuntu_24.ini diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index aebc4baf..00000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: - - master - - development - pull_request: - branches: - - master - - development - schedule: - - cron: '32 11 * * 6' - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - - permissions: - actions: read - contents: read - security-events: write - - steps: - - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2 - # Initializes the CodeQL tools for scanning. - - - name: Initialize CodeQL - uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba #v4.35.5 - with: - languages: 'python' - - - name: Autobuild - uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba #v4.35.5 - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba #v4.35.5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0519ecfc..1321c81f 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,7 +20,7 @@ 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!" @@ -52,12 +47,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 @@ -83,22 +72,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..7c9b4193 --- /dev/null +++ b/test/helpers/mocks.bash @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +# Mock command helpers for BATS tests. +# +# Each function writes a bash case-statement script to /usr/local/bin/ +# in the current environment, allowing tests to intercept command invocations. +# +# Usage: +# mock_command SCRIPT ARG1 OUTPUT1 RC1 [ARG2 OUTPUT2 RC2 ...] +# mock_command_2 SCRIPT ARG1 OUTPUT1 RC1 [ARG2 OUTPUT2 RC2 ...] +# mock_command_passthrough 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 '*'). +# +# Write a generated script to /usr/local/bin and clear its log file. +_write_mock_local() { + local script_name="$1" script_content="$2" + printf '%s' "$script_content" > "/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 script_name="$1" + shift + + 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_local "$script_name" "$script" +} + +# mock_command_2 — matches on "$1 $2" (quoted pattern, quoted echo output) +mock_command_2() { + local script_name="$1" + shift + + 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_local "$script_name" "$script" +} + +# mock_command_passthrough — matches on $1; falls through to real binary for +# unmatched arguments +mock_command_passthrough() { + local script_name="$1" + shift + + # Find the real binary path before we shadow it + local orig_path + orig_path=$(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_local "$script_name" "$script" +} diff --git a/test/requirements.txt b/test/requirements.txt deleted file mode 100644 index 95e17e96..00000000 --- a/test/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -pyyaml == 6.0.3 -pytest == 9.0.3 -pytest-xdist == 3.8.0 -pytest-testinfra == 10.2.2 -tox == 4.52.1 -pytest-clarity == 1.0.1 diff --git a/test/run.sh b/test/run.sh new file mode 100755 index 00000000..927608a7 --- /dev/null +++ b/test/run.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +# --------------------------------------------------------------------------- +# CLI arguments (optional) +# --------------------------------------------------------------------------- + +usage() { + echo "Usage:" + echo " DISTRO= bash test/run.sh" + echo " bash test/run.sh --distro " + echo "" + echo "Options:" + echo " -d, --distro Distro to test (e.g., debian_12)" + echo " -h, --help Show this help" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -d|--distro) + shift + if [[ $# -eq 0 ]]; then + echo "Error: --distro requires a value" + usage + exit 1 + fi + DISTRO="$1" + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Error: Unknown option '$1'" + usage + exit 1 + ;; + esac + shift +done + +# --------------------------------------------------------------------------- +# Distro selection +# --------------------------------------------------------------------------- + +if [[ -z "${DISTRO:-}" ]]; then + echo "Error: DISTRO is required." + echo "Example: DISTRO=debian_12 bash test/run.sh" + echo "or: bash test/run.sh --distro debian_12" + 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") + +DIRECT_TEST_FILES=( + test_automated_install.bats + test_ftl.bats + test_network.bats + test_utils.bats +) +[[ "$DISTRO_FAMILY" == "rhel" ]] && DIRECT_TEST_FILES+=(test_selinux.bats) + +# --------------------------------------------------------------------------- +# Build the test image +# --------------------------------------------------------------------------- + +IMAGE_TAG="pihole_test:${DISTRO}" + +docker buildx build \ + --load \ + --progress plain \ + -f "$DOCKERFILE" \ + -t "$IMAGE_TAG" \ + ../ + +DIRECT_TEST_FILES_CSV="$(IFS=,; echo "${DIRECT_TEST_FILES[*]}")" + +docker run --rm -t \ + -e BATS_CORE_REF="${BATS_CORE_REF:-v1.13.0}" \ + -e BATS_SUPPORT_REF="${BATS_SUPPORT_REF:-v0.3.0}" \ + -e BATS_ASSERT_REF="${BATS_ASSERT_REF:-v2.2.4}" \ + -e DIRECT_TEST_FILES_CSV="$DIRECT_TEST_FILES_CSV" \ + "$IMAGE_TAG" \ + bash -euo pipefail -c ' + cd /etc/.pihole/test + + mkdir -p libs + if [[ ! -d libs/bats ]]; then + git clone --depth=1 --single-branch --branch "${BATS_CORE_REF}" --quiet \ + https://github.com/bats-core/bats-core libs/bats + fi + if [[ ! -d libs/bats-support ]]; then + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_REF}" --quiet \ + https://github.com/bats-core/bats-support libs/bats-support + fi + if [[ ! -d libs/bats-assert ]]; then + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_REF}" --quiet \ + https://github.com/bats-core/bats-assert libs/bats-assert + fi + + IFS="," read -r -a direct_files <<< "$DIRECT_TEST_FILES_CSV" + + # Installer tests can mutate /etc/.pihole, so execute from a copied + # working tree to keep test files stable for the full run. + rm -rf /tmp/direct-tests + mkdir -p /tmp/direct-tests/libs /tmp/direct-tests/helpers + cp -a libs/bats /tmp/direct-tests/libs/ + cp -a libs/bats-support /tmp/direct-tests/libs/ + cp -a libs/bats-assert /tmp/direct-tests/libs/ + cp -a helpers/mocks.bash /tmp/direct-tests/helpers/ + + direct_files_local=() + for f in "${direct_files[@]}"; do + cp -a "$f" /tmp/direct-tests/ + direct_files_local+=("$(basename "$f")") + done + + cd /tmp/direct-tests + exec libs/bats/bin/bats --print-output-on-failure "${direct_files_local[@]}" + ' 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 e4646572..00000000 --- a/test/test_any_utils.py +++ /dev/null @@ -1,50 +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 - touch ./testoutput - 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..7a6a0f63 --- /dev/null +++ b/test/test_automated_install.bats @@ -0,0 +1,187 @@ +#!/usr/bin/env bats +# Core installer tests — package manager, cache, fresh install, dependencies + +load 'libs/bats-support/load' +load 'libs/bats-assert/load' +load 'helpers/mocks' + +TICK="[✓]" +CROSS="[✗]" +INFO="[i]" +FTL_BRANCH="development" + +setup() { + rm -f /usr/local/bin/dialog /usr/local/bin/git /usr/local/bin/systemctl /usr/local/bin/rc-service /usr/local/bin/apt-get + rm -f /var/log/dialog /var/log/git /var/log/systemctl /var/log/rc-service /var/log/apt-get + + # Restore any package managers disabled by tests. + [[ -e /usr/bin/apt-get.disabled ]] && mv -f /usr/bin/apt-get.disabled /usr/bin/apt-get || true + [[ -e /usr/bin/rpm.disabled ]] && mv -f /usr/bin/rpm.disabled /usr/bin/rpm || true + [[ -e /sbin/apk.disabled ]] && mv -f /sbin/apk.disabled /sbin/apk || true +} + +teardown() { + rm -f /usr/local/bin/dialog /usr/local/bin/git /usr/local/bin/systemctl /usr/local/bin/rc-service /usr/local/bin/apt-get + rm -f /var/log/dialog /var/log/git /var/log/systemctl /var/log/rc-service /var/log/apt-get + + [[ -e /usr/bin/apt-get.disabled ]] && mv -f /usr/bin/apt-get.disabled /usr/bin/apt-get || true + [[ -e /usr/bin/rpm.disabled ]] && mv -f /usr/bin/rpm.disabled /usr/bin/rpm || true + [[ -e /sbin/apk.disabled ]] && mv -f /sbin/apk.disabled /sbin/apk || true +} + +@test "installer exits when no supported package manager found" { + [[ -e /usr/bin/apt-get ]] && mv /usr/bin/apt-get /usr/bin/apt-get.disabled + [[ -e /usr/bin/rpm ]] && mv /usr/bin/rpm /usr/bin/rpm.disabled + [[ -e /sbin/apk ]] && mv /sbin/apk /sbin/apk.disabled + + run bash -c " + source /opt/pihole/basic-install.sh + package_manager_detect + " + + assert_output --partial "${CROSS} No supported package manager found" + assert_failure +} + +@test "installer continues when SELinux config file does not exist" { + run 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 dialog "*" "" "0" + mock_command_passthrough git "pull" "" "0" + mock_command_2 systemctl \ + "enable pihole-FTL" "" "0" \ + "restart pihole-FTL" "" "0" \ + "start pihole-FTL" "" "0" + mock_command_2 rc-service \ + "pihole-FTL enable" "" "0" \ + "pihole-FTL restart" "" "0" \ + "pihole-FTL start" "" "0" + + command -v apt-get > /dev/null && apt-get install -qq man || true + command -v dnf > /dev/null && dnf install -y man || true + command -v yum > /dev/null && yum install -y man || true + command -v apk > /dev/null && apk add mandoc man-pages || true + + echo "${FTL_BRANCH}" > /etc/pihole/ftlbranch + + run 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 + + local maninstalled=true + if [[ "$output" == *"${INFO} man not installed"* ]] || [[ "$output" == *"${INFO} man pages not installed"* ]]; then + maninstalled=false + fi + + local piholeuser="pihole" + _check_perm() { su -s /bin/bash -c "test -${1} ${2}" -p ${piholeuser}; } + + run _check_perm r /etc/pihole; assert_success + run _check_perm x /etc/pihole; assert_success + run _check_perm r /etc/pihole/dhcp.leases; assert_success + run _check_perm r /etc/pihole/install.log; assert_success + run _check_perm r /etc/pihole/versions; assert_success + run _check_perm r /etc/pihole/macvendor.db; assert_success + run _check_perm x /etc/init.d/pihole-FTL; assert_success + run _check_perm r /etc/init.d/pihole-FTL; assert_success + + 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 + + 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 + + local dirs + dirs=$(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=$(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 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 apt-get "update" "" "1" + + run 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." +} + +@test "OS can install required Pi-hole dependency packages" { + mock_command dialog "*" "" "0" + + run 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 dialog "*" "" "0" + + run bash -c " + export DEBIAN_FRONTEND=noninteractive + source /opt/pihole/basic-install.sh + package_manager_detect + update_package_cache + build_dependency_package + install_dependent_packages + " + assert_success + + run bash -c " + export DEBIAN_FRONTEND=noninteractive + source /opt/pihole/basic-install.sh + package_manager_detect + eval \"\${PKG_REMOVE}\" pihole-meta + " + 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_ftl.bats b/test/test_ftl.bats new file mode 100755 index 00000000..6d2fe843 --- /dev/null +++ b/test/test_ftl.bats @@ -0,0 +1,105 @@ +#!/usr/bin/env bats +# FTL architecture detection and binary installation tests + +load 'libs/bats-support/load' +load 'libs/bats-assert/load' +load 'helpers/mocks' + +TICK="[✓]" +INFO="[i]" + +FTL_BRANCH="development" + +setup() { + rm -f /usr/local/bin/uname /usr/local/bin/readelf /var/log/uname /var/log/readelf +} + +teardown() { + rm -f /usr/local/bin/uname /usr/local/bin/readelf /var/log/uname /var/log/readelf +} + +# --------------------------------------------------------------------------- +# FTL architecture detection — one @test per arch (replaces parametrize) +# --------------------------------------------------------------------------- + +_test_ftl_arch() { + local arch="$1" detected_string="$2" supported="$3" + + mock_command uname "-m" "$arch" "0" + 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" + echo "${FTL_BRANCH}" > /etc/pihole/ftlbranch + + run 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" + + if [[ "$output" != *"Downloading and Installing FTL"* && "$output" != *"Local binary up-to-date. No need to download!"* ]]; then + echo "Expected either download or up-to-date path, got:" >&2 + echo "$output" >&2 + false + fi + 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" { + echo "${FTL_BRANCH}" > /etc/pihole/ftlbranch + 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 bash -c ' + VERSION=$(pihole-FTL version) + echo "${VERSION:0:1}" + ' + assert_output --partial "v" +} diff --git a/test/test_network.bats b/test/test_network.bats new file mode 100755 index 00000000..4831118b --- /dev/null +++ b/test/test_network.bats @@ -0,0 +1,112 @@ +#!/usr/bin/env bats +# Network detection tests — IPv6 address detection and IP validation + +load 'libs/bats-support/load' +load 'libs/bats-assert/load' +load 'helpers/mocks' + +setup() { + rm -f /usr/local/bin/ip /var/log/ip +} + +teardown() { + rm -f /usr/local/bin/ip /var/log/ip +} + +# --------------------------------------------------------------------------- +# IPv6 detection +# --------------------------------------------------------------------------- + +@test "IPv6 link-local only: blocking disabled" { + mock_command_2 ip \ + "-6 address" "inet6 fe80::d210:52fa:fe00:7ad7/64 scope link" "0" + run 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 ip \ + "-6 address" "inet6 fda2:2001:5555:0:d210:52fa:fe00:7ad7/64 scope global" "0" + run 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 ip \ + "-6 address" "inet6 2003:12:1e43:301:d210:52fa:fe00:7ad7/64 scope global" "0" + run 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 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 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 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 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 bash -c "source /opt/pihole/basic-install.sh; valid_ip '${1}'" + assert_success + } + _invalid() { + run 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" +} diff --git a/test/test_selinux.bats b/test/test_selinux.bats new file mode 100755 index 00000000..82b8b903 --- /dev/null +++ b/test/test_selinux.bats @@ -0,0 +1,58 @@ +#!/usr/bin/env bats +# Tests for SELinux handling in basic-install.sh. +# 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="[✗]" + +setup() { + rm -f /usr/local/bin/getenforce /var/log/getenforce +} + +teardown() { + rm -f /usr/local/bin/getenforce /var/log/getenforce +} + +_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 getenforce "*" "$capitalized" "0" + mkdir -p /etc/selinux + echo "SELINUX=${state}" > /etc/selinux/config +} + +@test "SELinux enforcing: installer exits with error" { + _mock_selinux_config "enforcing" + run 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 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 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..2900c6ec --- /dev/null +++ b/test/test_utils.bats @@ -0,0 +1,52 @@ +#!/usr/bin/env bats +# Tests for utils.sh + +load 'libs/bats-support/load' +load 'libs/bats-assert/load' + +setup() { + rm -f ./testoutput +} + +# --------------------------------------------------------------------------- + +@test "addOrEditKeyValPair adds and replaces key-value pairs correctly" { + 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 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 bash -c " + source /opt/pihole/utils.sh + getFTLPID + " + assert_output "-1" +} + +@test "setFTLConfigValue and getFTLConfigValue round-trip" { + # FTL must be installed for this test + 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 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 From 1359c6aff51ad36c43eb670261798887c99d211d Mon Sep 17 00:00:00 2001 From: Adam Warner Date: Mon, 13 Apr 2026 22:20:58 +0100 Subject: [PATCH 09/14] test: simplify in-container BATS flow and clarify suite intent The test refactor intentionally prioritizes deterministic CI behavior and clearer semantics over historical transition scaffolding. Reasoning behind the changes: - Keep output consistent across distros by enabling pretty BATS output in all images; Alpine now installs ncurses so tput is available. - Remove no-longer-needed migration plumbing in test/run.sh (CSV env handoff and extra local filename array), while preserving isolated /tmp tests execution to avoid installer side effects on /etc/.pihole. - Rename ambiguous terminology from earlier direct-vs-legacy comparison and keep runner naming minimal (TEST_FILES, /tmp/tests). - Clarify scope of the former FTL suite by renaming it to test_installer_ftl.bats and updating test titles to reflect that these validate installer architecture-detection/install paths. - Reduce duplicated setup/teardown cleanup code in BATS files via shared reset helpers without changing behavior. Signed-off-by: Adam Warner --- test/_alpine_3_21.Dockerfile | 2 +- test/_alpine_3_22.Dockerfile | 2 +- test/_alpine_3_23.Dockerfile | 2 +- test/run.sh | 37 ++++++++----------- test/test_automated_install.bats | 13 +++---- ...{test_ftl.bats => test_installer_ftl.bats} | 30 ++++++++------- test/test_network.bats | 8 +++- test/test_selinux.bats | 8 +++- test/test_utils.bats | 10 ++++- 9 files changed, 63 insertions(+), 49 deletions(-) rename test/{test_ftl.bats => test_installer_ftl.bats} (77%) mode change 100755 => 100644 diff --git a/test/_alpine_3_21.Dockerfile b/test/_alpine_3_21.Dockerfile index d7b88f20..f38e9739 100644 --- a/test/_alpine_3_21.Dockerfile +++ b/test/_alpine_3_21.Dockerfile @@ -3,7 +3,7 @@ FROM alpine:3.21 ENV GITDIR=/etc/.pihole ENV SCRIPTDIR=/opt/pihole RUN sed -i 's/#\(.*\/community\)/\1/' /etc/apk/repositories -RUN apk --no-cache add bash coreutils curl git jq openrc shadow +RUN apk --no-cache add bash coreutils curl git jq ncurses openrc shadow RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole ADD . $GITDIR diff --git a/test/_alpine_3_22.Dockerfile b/test/_alpine_3_22.Dockerfile index 25beb4e0..f244f909 100644 --- a/test/_alpine_3_22.Dockerfile +++ b/test/_alpine_3_22.Dockerfile @@ -3,7 +3,7 @@ FROM alpine:3.22 ENV GITDIR=/etc/.pihole ENV SCRIPTDIR=/opt/pihole RUN sed -i 's/#\(.*\/community\)/\1/' /etc/apk/repositories -RUN apk --no-cache add bash coreutils curl git jq openrc shadow +RUN apk --no-cache add bash coreutils curl git jq ncurses openrc shadow RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole ADD . $GITDIR diff --git a/test/_alpine_3_23.Dockerfile b/test/_alpine_3_23.Dockerfile index 2cb34137..73c8db2d 100644 --- a/test/_alpine_3_23.Dockerfile +++ b/test/_alpine_3_23.Dockerfile @@ -3,7 +3,7 @@ FROM alpine:3.23 ENV GITDIR=/etc/.pihole ENV SCRIPTDIR=/opt/pihole RUN sed -i 's/#\(.*\/community\)/\1/' /etc/apk/repositories -RUN apk --no-cache add bash coreutils curl git jq openrc shadow +RUN apk --no-cache add bash coreutils curl git jq ncurses openrc shadow RUN mkdir -p $GITDIR $SCRIPTDIR /etc/pihole ADD . $GITDIR diff --git a/test/run.sh b/test/run.sh index 927608a7..026dac05 100755 --- a/test/run.sh +++ b/test/run.sh @@ -75,13 +75,13 @@ distro_family() { } DISTRO_FAMILY=$(distro_family "$DISTRO") -DIRECT_TEST_FILES=( +TEST_FILES=( test_automated_install.bats - test_ftl.bats + test_installer_ftl.bats test_network.bats test_utils.bats ) -[[ "$DISTRO_FAMILY" == "rhel" ]] && DIRECT_TEST_FILES+=(test_selinux.bats) +[[ "$DISTRO_FAMILY" == "rhel" ]] && TEST_FILES+=(test_selinux.bats) # --------------------------------------------------------------------------- # Build the test image @@ -96,17 +96,16 @@ docker buildx build \ -t "$IMAGE_TAG" \ ../ -DIRECT_TEST_FILES_CSV="$(IFS=,; echo "${DIRECT_TEST_FILES[*]}")" - docker run --rm -t \ -e BATS_CORE_REF="${BATS_CORE_REF:-v1.13.0}" \ -e BATS_SUPPORT_REF="${BATS_SUPPORT_REF:-v0.3.0}" \ -e BATS_ASSERT_REF="${BATS_ASSERT_REF:-v2.2.4}" \ - -e DIRECT_TEST_FILES_CSV="$DIRECT_TEST_FILES_CSV" \ "$IMAGE_TAG" \ bash -euo pipefail -c ' cd /etc/.pihole/test + test_files=("$@") + mkdir -p libs if [[ ! -d libs/bats ]]; then git clone --depth=1 --single-branch --branch "${BATS_CORE_REF}" --quiet \ @@ -121,23 +120,19 @@ docker run --rm -t \ https://github.com/bats-core/bats-assert libs/bats-assert fi - IFS="," read -r -a direct_files <<< "$DIRECT_TEST_FILES_CSV" - # Installer tests can mutate /etc/.pihole, so execute from a copied # working tree to keep test files stable for the full run. - rm -rf /tmp/direct-tests - mkdir -p /tmp/direct-tests/libs /tmp/direct-tests/helpers - cp -a libs/bats /tmp/direct-tests/libs/ - cp -a libs/bats-support /tmp/direct-tests/libs/ - cp -a libs/bats-assert /tmp/direct-tests/libs/ - cp -a helpers/mocks.bash /tmp/direct-tests/helpers/ + rm -rf /tmp/tests + mkdir -p /tmp/tests/libs /tmp/tests/helpers + cp -a libs/bats /tmp/tests/libs/ + cp -a libs/bats-support /tmp/tests/libs/ + cp -a libs/bats-assert /tmp/tests/libs/ + cp -a helpers/mocks.bash /tmp/tests/helpers/ - direct_files_local=() - for f in "${direct_files[@]}"; do - cp -a "$f" /tmp/direct-tests/ - direct_files_local+=("$(basename "$f")") + for f in "${test_files[@]}"; do + cp -a "$f" /tmp/tests/ done - cd /tmp/direct-tests - exec libs/bats/bin/bats --print-output-on-failure "${direct_files_local[@]}" - ' + cd /tmp/tests + exec libs/bats/bin/bats -p --print-output-on-failure "${test_files[@]##*/}" + ' bash "${TEST_FILES[@]}" diff --git a/test/test_automated_install.bats b/test/test_automated_install.bats index 7a6a0f63..6a676d09 100755 --- a/test/test_automated_install.bats +++ b/test/test_automated_install.bats @@ -10,7 +10,7 @@ CROSS="[✗]" INFO="[i]" FTL_BRANCH="development" -setup() { +_reset_automated_install_test_state() { rm -f /usr/local/bin/dialog /usr/local/bin/git /usr/local/bin/systemctl /usr/local/bin/rc-service /usr/local/bin/apt-get rm -f /var/log/dialog /var/log/git /var/log/systemctl /var/log/rc-service /var/log/apt-get @@ -20,13 +20,12 @@ setup() { [[ -e /sbin/apk.disabled ]] && mv -f /sbin/apk.disabled /sbin/apk || true } -teardown() { - rm -f /usr/local/bin/dialog /usr/local/bin/git /usr/local/bin/systemctl /usr/local/bin/rc-service /usr/local/bin/apt-get - rm -f /var/log/dialog /var/log/git /var/log/systemctl /var/log/rc-service /var/log/apt-get +setup() { + _reset_automated_install_test_state +} - [[ -e /usr/bin/apt-get.disabled ]] && mv -f /usr/bin/apt-get.disabled /usr/bin/apt-get || true - [[ -e /usr/bin/rpm.disabled ]] && mv -f /usr/bin/rpm.disabled /usr/bin/rpm || true - [[ -e /sbin/apk.disabled ]] && mv -f /sbin/apk.disabled /sbin/apk || true +teardown() { + _reset_automated_install_test_state } @test "installer exits when no supported package manager found" { diff --git a/test/test_ftl.bats b/test/test_installer_ftl.bats old mode 100755 new mode 100644 similarity index 77% rename from test/test_ftl.bats rename to test/test_installer_ftl.bats index 6d2fe843..7894582b --- a/test/test_ftl.bats +++ b/test/test_installer_ftl.bats @@ -1,5 +1,5 @@ #!/usr/bin/env bats -# FTL architecture detection and binary installation tests +# Installer tests for FTL architecture detection and binary installation load 'libs/bats-support/load' load 'libs/bats-assert/load' @@ -10,16 +10,20 @@ INFO="[i]" FTL_BRANCH="development" -setup() { +_reset_ftl_test_state() { rm -f /usr/local/bin/uname /usr/local/bin/readelf /var/log/uname /var/log/readelf } +setup() { + _reset_ftl_test_state +} + teardown() { - rm -f /usr/local/bin/uname /usr/local/bin/readelf /var/log/uname /var/log/readelf + _reset_ftl_test_state } # --------------------------------------------------------------------------- -# FTL architecture detection — one @test per arch (replaces parametrize) +# Installer FTL architecture detection — one @test per arch # --------------------------------------------------------------------------- _test_ftl_arch() { @@ -55,39 +59,39 @@ _test_ftl_arch() { fi } -@test "FTL detects aarch64 architecture" { +@test "installer detects aarch64 architecture for FTL" { _test_ftl_arch "aarch64" "AArch64 (64 Bit ARM)" "true" } -@test "FTL detects ARMv6 architecture" { +@test "installer detects ARMv6 architecture for FTL" { _test_ftl_arch "armv6" "ARMv6" "true" } -@test "FTL detects ARMv7l architecture" { +@test "installer detects ARMv7l architecture for FTL" { _test_ftl_arch "armv7l" "ARMv7 (or newer)" "true" } -@test "FTL detects ARMv7 architecture" { +@test "installer detects ARMv7 architecture for FTL" { _test_ftl_arch "armv7" "ARMv7 (or newer)" "true" } -@test "FTL detects ARMv8a architecture" { +@test "installer detects ARMv8a architecture for FTL" { _test_ftl_arch "armv8a" "ARMv7 (or newer)" "true" } -@test "FTL detects x86_64 architecture" { +@test "installer detects x86_64 architecture for FTL" { _test_ftl_arch "x86_64" "x86_64" "true" } -@test "FTL detects riscv64 architecture" { +@test "installer detects riscv64 architecture for FTL" { _test_ftl_arch "riscv64" "riscv64" "true" } -@test "FTL reports unsupported architecture" { +@test "installer reports unsupported architecture for FTL" { _test_ftl_arch "mips" "mips" "false" } -@test "FTL development binary is installed and responsive" { +@test "installer provides a responsive FTL development binary" { echo "${FTL_BRANCH}" > /etc/pihole/ftlbranch bash -c " source /opt/pihole/basic-install.sh diff --git a/test/test_network.bats b/test/test_network.bats index 4831118b..a065a63d 100755 --- a/test/test_network.bats +++ b/test/test_network.bats @@ -5,12 +5,16 @@ load 'libs/bats-support/load' load 'libs/bats-assert/load' load 'helpers/mocks' -setup() { +_reset_network_test_state() { rm -f /usr/local/bin/ip /var/log/ip } +setup() { + _reset_network_test_state +} + teardown() { - rm -f /usr/local/bin/ip /var/log/ip + _reset_network_test_state } # --------------------------------------------------------------------------- diff --git a/test/test_selinux.bats b/test/test_selinux.bats index 82b8b903..bd65868c 100755 --- a/test/test_selinux.bats +++ b/test/test_selinux.bats @@ -9,12 +9,16 @@ load 'helpers/mocks' TICK="[✓]" CROSS="[✗]" -setup() { +_reset_selinux_test_state() { rm -f /usr/local/bin/getenforce /var/log/getenforce } +setup() { + _reset_selinux_test_state +} + teardown() { - rm -f /usr/local/bin/getenforce /var/log/getenforce + _reset_selinux_test_state } _mock_selinux_config() { diff --git a/test/test_utils.bats b/test/test_utils.bats index 2900c6ec..088ea0f7 100755 --- a/test/test_utils.bats +++ b/test/test_utils.bats @@ -4,10 +4,18 @@ load 'libs/bats-support/load' load 'libs/bats-assert/load' -setup() { +_reset_utils_test_state() { rm -f ./testoutput } +setup() { + _reset_utils_test_state +} + +teardown() { + _reset_utils_test_state +} + # --------------------------------------------------------------------------- @test "addOrEditKeyValPair adds and replaces key-value pairs correctly" { From 356afe66c514f6edc182569bbc10bbe00b648e13 Mon Sep 17 00:00:00 2001 From: Adam Warner Date: Mon, 13 Apr 2026 22:24:21 +0100 Subject: [PATCH 10/14] test: restore executable bit on installer FTL bats file Signed-off-by: Adam Warner --- test/test_installer_ftl.bats | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 test/test_installer_ftl.bats diff --git a/test/test_installer_ftl.bats b/test/test_installer_ftl.bats old mode 100644 new mode 100755 From 551b6516d10a47b5dcf6b9d2b268d809ca55e3d6 Mon Sep 17 00:00:00 2001 From: Adam Warner Date: Thu, 16 Apr 2026 20:48:34 +0100 Subject: [PATCH 11/14] test: bake BATS libraries into test images at build time Add ARG declarations and a build-time RUN step to all Dockerfiles so bats-core, bats-support, bats-assert, bats-mock, and bats-file are cloned at image build time rather than at container runtime. Library versions are defined once in run.sh and passed down via --build-arg Add .dockerignore to exclude any locally-cached test/libs/ directory from the build context, preventing 'destination already exists' failures when the directory exists from a previous local test run. Remove the legacy commented-out sed stub command from all Dockerfiles Signed-off-by: Adam Warner --- .dockerignore | 1 + test/_alpine_3_21.Dockerfile | 12 +++++++++++- test/_alpine_3_22.Dockerfile | 12 +++++++++++- test/_alpine_3_23.Dockerfile | 12 +++++++++++- test/_centos_10.Dockerfile | 12 +++++++++++- test/_centos_9.Dockerfile | 12 +++++++++++- test/_debian_11.Dockerfile | 12 +++++++++++- test/_debian_12.Dockerfile | 12 +++++++++++- test/_debian_13.Dockerfile | 12 +++++++++++- test/_fedora_40.Dockerfile | 12 +++++++++++- test/_fedora_41.Dockerfile | 12 +++++++++++- test/_fedora_42.Dockerfile | 12 +++++++++++- test/_fedora_43.Dockerfile | 12 +++++++++++- test/_ubuntu_20.Dockerfile | 12 +++++++++++- test/_ubuntu_22.Dockerfile | 12 +++++++++++- test/_ubuntu_24.Dockerfile | 12 +++++++++++- 16 files changed, 166 insertions(+), 15 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..d9c5a6ee --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +test/libs/ diff --git a/test/_alpine_3_21.Dockerfile b/test/_alpine_3_21.Dockerfile index f38e9739..18480ed5 100644 --- a/test/_alpine_3_21.Dockerfile +++ b/test/_alpine_3_21.Dockerfile @@ -13,6 +13,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_alpine_3_22.Dockerfile b/test/_alpine_3_22.Dockerfile index f244f909..53b12eb9 100644 --- a/test/_alpine_3_22.Dockerfile +++ b/test/_alpine_3_22.Dockerfile @@ -13,6 +13,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_alpine_3_23.Dockerfile b/test/_alpine_3_23.Dockerfile index 73c8db2d..542de879 100644 --- a/test/_alpine_3_23.Dockerfile +++ b/test/_alpine_3_23.Dockerfile @@ -13,6 +13,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_centos_10.Dockerfile b/test/_centos_10.Dockerfile index 78a89789..0b8747ed 100644 --- a/test/_centos_10.Dockerfile +++ b/test/_centos_10.Dockerfile @@ -14,6 +14,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_centos_9.Dockerfile b/test/_centos_9.Dockerfile index 73f53fa5..317333af 100644 --- a/test/_centos_9.Dockerfile +++ b/test/_centos_9.Dockerfile @@ -14,6 +14,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_debian_11.Dockerfile b/test/_debian_11.Dockerfile index 2389063c..b5f26d1d 100644 --- a/test/_debian_11.Dockerfile +++ b/test/_debian_11.Dockerfile @@ -11,6 +11,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_debian_12.Dockerfile b/test/_debian_12.Dockerfile index a6c5f1ed..9fccc645 100644 --- a/test/_debian_12.Dockerfile +++ b/test/_debian_12.Dockerfile @@ -11,6 +11,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_debian_13.Dockerfile b/test/_debian_13.Dockerfile index cfff2235..743ff9d7 100644 --- a/test/_debian_13.Dockerfile +++ b/test/_debian_13.Dockerfile @@ -11,6 +11,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_fedora_40.Dockerfile b/test/_fedora_40.Dockerfile index 43913895..e5f33d63 100644 --- a/test/_fedora_40.Dockerfile +++ b/test/_fedora_40.Dockerfile @@ -12,6 +12,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_fedora_41.Dockerfile b/test/_fedora_41.Dockerfile index c03371a5..ee1254e6 100644 --- a/test/_fedora_41.Dockerfile +++ b/test/_fedora_41.Dockerfile @@ -12,6 +12,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_fedora_42.Dockerfile b/test/_fedora_42.Dockerfile index 90b17c0b..4d9a072f 100644 --- a/test/_fedora_42.Dockerfile +++ b/test/_fedora_42.Dockerfile @@ -12,6 +12,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_fedora_43.Dockerfile b/test/_fedora_43.Dockerfile index 85f06ff8..fa400c7a 100644 --- a/test/_fedora_43.Dockerfile +++ b/test/_fedora_43.Dockerfile @@ -12,6 +12,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_ubuntu_20.Dockerfile b/test/_ubuntu_20.Dockerfile index 5b8deb5d..7b0850fb 100644 --- a/test/_ubuntu_20.Dockerfile +++ b/test/_ubuntu_20.Dockerfile @@ -11,6 +11,16 @@ ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$SCRIPTDIR RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_ubuntu_22.Dockerfile b/test/_ubuntu_22.Dockerfile index c3be89e1..53040d35 100644 --- a/test/_ubuntu_22.Dockerfile +++ b/test/_ubuntu_22.Dockerfile @@ -12,6 +12,16 @@ ENV DEBIAN_FRONTEND=noninteractive RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ diff --git a/test/_ubuntu_24.Dockerfile b/test/_ubuntu_24.Dockerfile index cf57c2aa..098e4760 100644 --- a/test/_ubuntu_24.Dockerfile +++ b/test/_ubuntu_24.Dockerfile @@ -12,6 +12,16 @@ ENV DEBIAN_FRONTEND=noninteractive RUN true && \ chmod +x $SCRIPTDIR/* +ARG BATS_CORE_VER +ARG BATS_SUPPORT_VER +ARG BATS_ASSERT_VER +ARG BATS_MOCK_VER +ARG BATS_FILE_VER +RUN git clone --depth=1 --single-branch --branch "${BATS_CORE_VER}" https://github.com/bats-core/bats-core $GITDIR/test/libs/bats && \ + git clone --depth=1 --single-branch --branch "${BATS_SUPPORT_VER}" https://github.com/bats-core/bats-support $GITDIR/test/libs/bats-support && \ + git clone --depth=1 --single-branch --branch "${BATS_ASSERT_VER}" https://github.com/bats-core/bats-assert $GITDIR/test/libs/bats-assert && \ + git clone --depth=1 --single-branch --branch "${BATS_MOCK_VER}" https://github.com/jasonkarns/bats-mock $GITDIR/test/libs/bats-mock && \ + git clone --depth=1 --single-branch --branch "${BATS_FILE_VER}" https://github.com/bats-core/bats-file $GITDIR/test/libs/bats-file + ENV SKIP_INSTALL=true -#sed '/# Start the installer/Q' /opt/pihole/basic-install.sh > /opt/pihole/stub_basic-install.sh && \ From e972c758abee2bc88fa449898ee6cf70e71fe0ec Mon Sep 17 00:00:00 2001 From: Adam Warner Date: Thu, 16 Apr 2026 20:48:50 +0100 Subject: [PATCH 12/14] test: split containers, simplify run_suite, improve distro error Split the BATS suite into two containers: one for mock/function tests and one for the fresh install, so installer tests can mutate the filesystem freely without teardown. Replace mocks.bash with bats-mock and bats-file, baked into the test images at build time via Docker ARG-versioned git clones. Improve the invalid-distro error to list available distros rather than printing an opaque Dockerfile path. Signed-off-by: Adam Warner --- test/helpers/mocks.bash | 102 ------------------------- test/run.sh | 114 +++++++++++++++++----------- test/test_automated_install.bats | 97 ++---------------------- test/test_fresh_install.bats | 124 +++++++++++++++++++++++++++++++ test/test_installer_ftl.bats | 31 ++++---- test/test_network.bats | 29 ++------ test/test_selinux.bats | 14 +--- test/test_utils.bats | 27 ++++--- 8 files changed, 240 insertions(+), 298 deletions(-) delete mode 100755 test/helpers/mocks.bash create mode 100755 test/test_fresh_install.bats diff --git a/test/helpers/mocks.bash b/test/helpers/mocks.bash deleted file mode 100755 index 7c9b4193..00000000 --- a/test/helpers/mocks.bash +++ /dev/null @@ -1,102 +0,0 @@ -#!/usr/bin/env bash -# Mock command helpers for BATS tests. -# -# Each function writes a bash case-statement script to /usr/local/bin/ -# in the current environment, allowing tests to intercept command invocations. -# -# Usage: -# mock_command SCRIPT ARG1 OUTPUT1 RC1 [ARG2 OUTPUT2 RC2 ...] -# mock_command_2 SCRIPT ARG1 OUTPUT1 RC1 [ARG2 OUTPUT2 RC2 ...] -# mock_command_passthrough 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 '*'). -# -# Write a generated script to /usr/local/bin and clear its log file. -_write_mock_local() { - local script_name="$1" script_content="$2" - printf '%s' "$script_content" > "/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 script_name="$1" - shift - - 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_local "$script_name" "$script" -} - -# mock_command_2 — matches on "$1 $2" (quoted pattern, quoted echo output) -mock_command_2() { - local script_name="$1" - shift - - 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_local "$script_name" "$script" -} - -# mock_command_passthrough — matches on $1; falls through to real binary for -# unmatched arguments -mock_command_passthrough() { - local script_name="$1" - shift - - # Find the real binary path before we shadow it - local orig_path - orig_path=$(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_local "$script_name" "$script" -} diff --git a/test/run.sh b/test/run.sh index 026dac05..a07b8c25 100755 --- a/test/run.sh +++ b/test/run.sh @@ -2,7 +2,7 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -cd "$SCRIPT_DIR" +cd "${SCRIPT_DIR}" # --------------------------------------------------------------------------- # CLI arguments (optional) @@ -18,6 +18,10 @@ usage() { echo " -h, --help Show this help" } +list_distros() { + find . -maxdepth 1 -name '_*.Dockerfile' | sed 's|^\./||;s/^_//;s/\.Dockerfile$//' | sort +} + while [[ $# -gt 0 ]]; do case "$1" in -d|--distro) @@ -31,6 +35,9 @@ while [[ $# -gt 0 ]]; do ;; -h|--help) usage + echo "" + echo "Available distros:" + list_distros exit 0 ;; *) @@ -52,13 +59,14 @@ if [[ -z "${DISTRO:-}" ]]; then echo "or: bash test/run.sh --distro debian_12" echo "" echo "Available distros:" - ls _*.Dockerfile | sed 's/^_//;s/\.Dockerfile$//' | sort + list_distros exit 1 fi DOCKERFILE="_${DISTRO}.Dockerfile" -if [[ ! -f "$DOCKERFILE" ]]; then - echo "Error: Dockerfile not found: $DOCKERFILE" +if [[ ! -f "${DOCKERFILE}" ]]; then + echo "Error: Unknown distro '${DISTRO}'. Available distros:" + list_distros | sed 's/^/ /' exit 1 fi @@ -73,18 +81,43 @@ distro_family() { *) echo "debian" ;; esac } -DISTRO_FAMILY=$(distro_family "$DISTRO") +DISTRO_FAMILY=$(distro_family "${DISTRO}") -TEST_FILES=( +# --------------------------------------------------------------------------- +# Suite definitions +# +# Suite 1 — mock/function tests: run together in one container. Each test +# cleans up its own state via setup()/teardown(); no full install occurs. +# +# Suite 2 — fresh install: runs alone in its own container so the installer +# can mutate the filesystem freely without needing any teardown. +# --------------------------------------------------------------------------- + +SUITE_1=( test_automated_install.bats test_installer_ftl.bats test_network.bats test_utils.bats ) -[[ "$DISTRO_FAMILY" == "rhel" ]] && TEST_FILES+=(test_selinux.bats) +[[ "${DISTRO_FAMILY}" == "rhel" ]] && SUITE_1+=(test_selinux.bats) + +SUITE_2=(test_fresh_install.bats) # --------------------------------------------------------------------------- -# Build the test image +# BATS library versions — single source of truth, passed to Docker as build +# args so the Dockerfiles themselves stay version-agnostic. Override any of +# these by setting the corresponding environment variable before calling this +# script, e.g. BATS_CORE_VER=v1.14.0 bash test/run.sh --distro debian_12 +# --------------------------------------------------------------------------- + +BATS_CORE_VER="${BATS_CORE_VER:-v1.13.0}" +BATS_SUPPORT_VER="${BATS_SUPPORT_VER:-v0.3.0}" +BATS_ASSERT_VER="${BATS_ASSERT_VER:-v2.2.4}" +BATS_MOCK_VER="${BATS_MOCK_VER:-v1.2.5}" +BATS_FILE_VER="${BATS_FILE_VER:-v0.4.0}" + +# --------------------------------------------------------------------------- +# Build the test image (once, shared by both suites) # --------------------------------------------------------------------------- IMAGE_TAG="pihole_test:${DISTRO}" @@ -92,47 +125,38 @@ IMAGE_TAG="pihole_test:${DISTRO}" docker buildx build \ --load \ --progress plain \ - -f "$DOCKERFILE" \ - -t "$IMAGE_TAG" \ + --build-arg "BATS_CORE_VER=${BATS_CORE_VER}" \ + --build-arg "BATS_SUPPORT_VER=${BATS_SUPPORT_VER}" \ + --build-arg "BATS_ASSERT_VER=${BATS_ASSERT_VER}" \ + --build-arg "BATS_MOCK_VER=${BATS_MOCK_VER}" \ + --build-arg "BATS_FILE_VER=${BATS_FILE_VER}" \ + -f "${DOCKERFILE}" \ + -t "${IMAGE_TAG}" \ ../ -docker run --rm -t \ - -e BATS_CORE_REF="${BATS_CORE_REF:-v1.13.0}" \ - -e BATS_SUPPORT_REF="${BATS_SUPPORT_REF:-v0.3.0}" \ - -e BATS_ASSERT_REF="${BATS_ASSERT_REF:-v2.2.4}" \ - "$IMAGE_TAG" \ - bash -euo pipefail -c ' - cd /etc/.pihole/test +# --------------------------------------------------------------------------- +# run_suite