mirror of
https://github.com/pi-hole/FTL.git
synced 2026-08-22 05:37:33 +01:00
The API test suite asserts exact DNS query counters. Several of them (`TOTAL`, `DNSKEY`, `DS`, `TOP_DOMAIN`) depend on how many DNSKEY/DS lookups dnsmasq issues while validating DNSSEC. Until now those lookups recursed to the live ICANN root, because FTL configures the real root trust anchors whenever `dns.dnssec` is enabled and the local PowerDNS recursor had no root zone of its own. The number of root DNSKEY queries therefore tracked ICANN's published root key set, so an ongoing key-signing-key rollover silently shifted the counters (9 -> 7 DNSKEY) and broke the suite even on unrelated PRs. We make the whole suite hermetic: 1. Serve a locally-signed root zone from PowerDNS, forward `.` to it and trust its key, so root DNSKEY validation resolves inside the test environment instead of reaching the internet. 2. Mark the locally-served *unsigned* zones (`icloud.com`, `apple-dns.net`, `in-addr.arpa`, `ip6.arpa`) as local domains, so dnsmasq no longer proves them unsigned by walking up to the real root. 3. Give the `bogus` zone a deliberately mismatched local trust anchor so it fails validation locally rather than by failing to find a secure delegation at the root. 4. Drop the root-key pre-warm `dig`, an internet round-trip that no longer serves any purpose. With no query leaving for the real root the counters are stable and independent of ICANN key rollovers, so they are recalibrated accordingly. Marking the extra zones as local emits the same "negative DS reply without NS record" warning already whitelisted for `ftl`, so the `test_final` whitelist is broadened to match it for any zone. Signed-off-by: DL6ER <dl6er@dl6er.de>
116 lines
5.6 KiB
Bash
116 lines
5.6 KiB
Bash
#!/usr/bin/env bats
|
|
# Final log validation and FTL termination tests.
|
|
# This file runs AFTER both test_suite.bats and the pytest API tests
|
|
# to catch any unexpected log messages produced during the entire run.
|
|
|
|
# Load BATS libraries for enhanced testing capabilities
|
|
bats_load_library 'bats-support'
|
|
bats_load_library 'bats-assert'
|
|
load 'bats_helper.bash'
|
|
|
|
@test "No WARNING messages in FTL.log (besides known warnings)" {
|
|
run bash -c 'grep "WARNING:" /var/log/pihole/FTL.log | grep -v -E "CAP_NET_ADMIN|CAP_NET_RAW|CAP_SYS_NICE|CAP_IPC_LOCK|CAP_CHOWN|CAP_NET_BIND_SERVICE|CAP_SYS_TIME|FTLCONF_|(negative DS reply without NS record received for )|(nameserver 127.0.0.1 refused to do a recursive query)|API: Config item is invalid|API: Config item validation failed|API: Not found|API: Config items set via environment variables|API: Rate-limiting login attempts|API: You need to specify both|API: No request body data|API: Invalid request|API: Rate-limiting 2FA token requests|2FA code has already been used|API: Reused 2FA token"'
|
|
refute_output
|
|
}
|
|
|
|
@test "No ERROR messages in FTL.log (besides known/intended errors)" {
|
|
run bash -c 'grep "ERROR: " /var/log/pihole/FTL.log | grep -v -E "(index\.html)|(Failed to create shared memory object)|(FTLCONF_debug_api is not a boolean)|(FTLCONF_files_pcap)|(Failed to set|adjust time during NTP sync: Insufficient permissions)|(nlrequest error)|(Failed to read ARP cache)"'
|
|
refute_output
|
|
}
|
|
|
|
@test "No CRIT messages in FTL.log (besides error due to starting FTL more than once)" {
|
|
run bash -c 'grep "CRIT:" /var/log/pihole/FTL.log | grep -v "CRIT: pihole-FTL is already running"'
|
|
refute_output
|
|
}
|
|
|
|
@test "No \"DB not available\" messages in FTL.log" {
|
|
run bash -c 'grep -c "database not available" /var/log/pihole/FTL.log'
|
|
assert_line --index 0 "0"
|
|
}
|
|
|
|
@test "Expected number of config file rotations" {
|
|
# BATS: 1x pihole.toml write (dns.reply.host API PATCH)
|
|
# BATS: 2x pihole.toml writes (CLI password set/remove processes)
|
|
# pytest: 3x pihole.toml writes (password, app_pwhash, serve_all via API)
|
|
# pytest: 2x pihole.toml writes (dns/hosts config array PUT + DELETE)
|
|
# pytest: 2x pihole.toml writes (dns/blocking disable + enable)
|
|
# pytest: 4x pihole.toml writes (config PATCH round-trips: bool + int, change + restore each)
|
|
# pytest: 2x pihole.toml writes (auth stress test password set + remove)
|
|
# pytest: 2x pihole.toml writes (TOTP stress test secret set + remove)
|
|
# pytest: 2x pihole.toml writes (auth security test password set + remove)
|
|
# pytest: 2x pihole.toml writes (auth security test TOTP secret set + remove)
|
|
run bash -c 'grep -c "INFO: Config file written to /etc/pihole/pihole.toml" /var/log/pihole/FTL.log'
|
|
printf "pihole.toml write count: %s\n" "${lines[0]}"
|
|
# On RISCV64, pytest is skipped (too slow), so only BATS writes occur
|
|
if [[ "${CI_ARCH}" == "linux/riscv64" ]]; then
|
|
assert_line --index 0 "1"
|
|
else
|
|
[[ ${lines[0]} == "22" ]]
|
|
fi
|
|
# CLI password set/remove trigger inotify reload but result in
|
|
# "pihole.toml unchanged" as the in-memory config already matches
|
|
run bash -c 'grep -c "pihole.toml unchanged" /var/log/pihole/FTL.log'
|
|
printf "pihole.toml unchanged count: %s\n" "${lines[0]}"
|
|
[[ ${lines[0]} -ge 2 ]]
|
|
assert_success
|
|
run bash -c 'grep -c "DEBUG_CONFIG: Config file written to /etc/pihole/dnsmasq.conf" /var/log/pihole/FTL.log'
|
|
printf "dnsmasq.conf write count: %s\n" "${lines[0]}"
|
|
assert_line --index 0 "1"
|
|
run bash -c 'grep -c "DEBUG_CONFIG: HOSTS file written to /etc/pihole/hosts/custom.list" /var/log/pihole/FTL.log'
|
|
printf "custom.list write count: %s\n" "${lines[0]}"
|
|
# On RISCV64, pytest is skipped, so only BATS writes occur (3x)
|
|
# Otherwise, pytest dns/hosts config array PUT + DELETE add 2 more (5x)
|
|
if [[ "${CI_ARCH}" == "linux/riscv64" ]]; then
|
|
assert_line --index 0 "3"
|
|
else
|
|
assert_line --index 0 "5"
|
|
fi
|
|
}
|
|
|
|
@test "Query with ID 0 has been saved to the database" {
|
|
# FTL exports queries from in-memory DB to disk after a configurable
|
|
# delay (default 30s). Poll up to 60s for the export to complete.
|
|
for i in $(seq 1 30); do
|
|
run bash -c './pihole-FTL sqlite3 /etc/pihole/pihole-FTL.db "SELECT COUNT(*) FROM queries WHERE id=0;"'
|
|
if [[ ${lines[0]} == "1" ]]; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
done
|
|
assert_line --index 0 "1"
|
|
}
|
|
|
|
@test "FTL terminates with message" {
|
|
logsize_before=$(stat -c%s /var/log/pihole/FTL.log)
|
|
# Kill pihole-FTL after having completed all tests
|
|
pid=$(cat /run/pihole-FTL.pid)
|
|
printf "Killing pihole-FTL with PID %s\n" "$pid"
|
|
|
|
run bash -c "kill $pid"
|
|
assert_success
|
|
|
|
# Wait until pihole-FTL has terminated
|
|
run bash -c "./pihole-FTL wait-for '########## FTL terminated after' /var/log/pihole/FTL.log 30 $logsize_before"
|
|
assert_success
|
|
}
|
|
|
|
@test "Shutdown reason logged at INFO level (#2818)" {
|
|
# Verify the shutdown path now logs at INFO level instead of DEBUG-only
|
|
run bash -c 'grep "INFO: Shutting down (exit code" /var/log/pihole/FTL.log'
|
|
assert_success
|
|
}
|
|
|
|
@test "SIGTERM source re-logged near final termination message (#2818)" {
|
|
# Verify the SIGTERM sender is re-logged during cleanup so it appears
|
|
# near the "FTL terminated" message even in truncated logs
|
|
run bash -c 'grep "INFO: Terminated by" /var/log/pihole/FTL.log'
|
|
assert_success
|
|
|
|
# Verify ordering: "Terminated by" must appear AFTER "Shutting down" and
|
|
# BEFORE the final "FTL terminated" message
|
|
run bash -c 'grep -n "Shutting down (exit code\|Terminated by\|FTL terminated after" /var/log/pihole/FTL.log | tail -3'
|
|
assert_line --partial --index 0 "Shutting down (exit code"
|
|
assert_line --partial --index 1 "Terminated by"
|
|
assert_line --partial --index 2 "FTL terminated after"
|
|
}
|