17 Commits
Author SHA1 Message Date
DL6ER 70aa28926b tests: serve a local root zone so DNSSEC validation is hermetic
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>
2026-07-11 22:13:18 +02:00
DL6ER 19de438450 Add security regression tests for auth and query hardening
Cover three fixes from the security pass: session cookies carry the Secure attribute only over TLS (and never over plain HTTP), an accepted TOTP code cannot be replayed, and /api/queries clamps an unbounded length parameter. The cookie and TOTP tests live in a self-contained module so they do not disturb the order-dependent workflow in test_z_auth.py; the TOTP test clears the shared secret via the session created by the accepted login so cleanup neither trips the replay guard nor emits a 2FA warning. Account for the module's four extra pihole.toml writes in test_final.bats.

Signed-off-by: DL6ER <dl6er@dl6er.de>
2026-07-04 20:25:14 +02:00
yubiuser 40da3cd0ae Fix BATS test of no ERRORS in FTL.log
Signed-off-by: yubiuser <github@yubiuser.dev>
2026-06-29 12:16:32 +02:00
yubiuser 47bb65625f Move on_failure_hook to separate file
Signed-off-by: yubiuser <github@yubiuser.dev>
2026-06-15 09:47:40 +02:00
yubiuser b1022a966f Use $BATS_LIB_PATH
Signed-off-by: yubiuser <github@yubiuser.dev>
2026-06-15 09:47:40 +02:00
yubiuser 2f34c5fc59 Add on_failure hook and remove debug logging from each test case
Signed-off-by: yubiuser <github@yubiuser.dev>
2026-06-15 09:47:40 +02:00
yubiuser be7e9a0307 Use bats_assert and bats_file
Signed-off-by: yubiuser <github@yubiuser.dev>
2026-06-15 09:46:56 +02:00
Dominik ec6f5d9e9b Adjust tests after recent dnsmasq changes
Signed-off-by: Dominik <dl6er@dl6er.de>
2026-06-12 19:42:16 +02:00
Dominik 46ff6fb937 Address review comments
Signed-off-by: Dominik <dl6er@dl6er.de>
2026-05-06 19:10:21 +02:00
Dominik 3952f05555 Improve shutdown diagnostics to identify SIGTERM source
Issue #2818 reports FTL terminating cleanly (code 0) after ~15-16 hours of
uptime, with the only captured log lines being:

  Thread webserver (7) is idle, terminating it.
  All threads joined
  ########## FTL terminated after 15h 50m 45s (code 0)! ##########

A thorough audit of all paths that can set the global `killed` flag confirms
there is no internal code path leading to spontaneous termination - every
shutdown is either test-mode (`pihole-FTL test`), a `die()`/longjmp from
dnsmasq, or an external SIGTERM mapped to dnsmasq's EVENT_TERM via SIGUSR6.
The "Thread webserver (7) is idle" message is a red herring: the webserver
cert-renewal thread sleeps for 24 hours between checks, so it is essentially
always cancellable when shutdown begins.

The reason the reporter could not diagnose the cause is that the relevant
information was either logged at DEBUG level (and therefore invisible) or
emitted far above the final termination message (and therefore lost when the
log was truncated). This commit closes both gaps so the next occurrence will
be diagnosable from a tail of FTL.log alone:

- signals.c: persist the SIGTERM sender info ("name (PID, user UID)") into
  a static buffer in SIGTERM_handler, exposed via get_term_source(). The
  buffer is sized 256 bytes and only ever read after being written by the
  signal handler, so no locking is needed.

- daemon.c: in cleanup(), re-log the stored SIGTERM source as
  "Terminated by ..." right before the final "FTL terminated after"
  banner. Even when the log is truncated to the last few lines, the
  termination source will now be visible alongside the final message.

- main.c: promote the "Shutting down (exit code, jmpret)" log from
  log_debug to log_info so the entry into the cleanup path is always
  recorded, not just under DEBUG_ANY.

- webserver/webserver.c: split the webserver thread's 24-hour
  cert-check sleep into 24 x 1-hour sleeps with intermediate `killed`
  checks. This is a robustness improvement, not a bug fix: the thread
  is no longer marked as cancellable for 24 hours straight, so the
  misleading "is idle" log on shutdown happens at most an hour after
  the last cert check rather than at any point in a 24-hour window.

- test/test_final.bats: add two regression tests that run after the
  existing "FTL terminates with message" test:
    * Verify "INFO: Shutting down (exit code" is present (catches
      accidental regression of the log_debug -> log_info change).
    * Verify "INFO: Terminated by" is present AND that the three key
      messages appear in order: Shutting down -> Terminated by ->
      FTL terminated after.

After this change, a SIGTERM-triggered shutdown produces:

  INFO: Asked to terminate by "systemd" (PID 1, user root UID 0)
  ...
  INFO: Shutting down (exit code 0, jmpret 0)
  INFO: Finished final database update
  INFO: Waiting for threads to join
  INFO: Thread webserver (7) is idle, terminating it.
  INFO: All threads joined
  INFO: Terminated by "systemd" (PID 1, user root UID 0)
  INFO: ########## FTL terminated after 15h 50m 45s (code 0)! ##########

Signed-off-by: Dominik <dl6er@dl6er.de>
2026-04-07 07:25:57 +02:00
Dominik a8415ffbd7 test: add parallel authentication stress test and gitignore test/libs
Add test/api/test_s_auth_stress.py exercising the auth subsystem under
concurrent load (12 threads, kept below max_sessions=16). Sessions are
created sequentially (respecting FTL's 3-attempts/s rate limit with
retry-on-429 backoff), then concurrency targets the thread-safety
surfaces:

- Parallel session validation (concurrent GET /api/auth with SIDs)
- Concurrent logout with cross-session isolation checks (logout half,
  verify other half survives)
- Mixed session check/logout operations from a shared session pool
- Concurrent wrong-password rejection (verifies no SIGSEGV; runs last
  since it intentionally triggers rate-limiting)

Every test cleans up its sessions to avoid exhausting max_sessions
across test boundaries. Bump expected config rotation count in
test_final.bats from 14 to 16 for the stress test's password
set/remove cycle.

Also add test/libs/ to .gitignore per review feedback — the directory is
populated at test time by test/run.sh cloning bats-core.

See alse: https://github.com/pi-hole/FTL/pull/2835

Signed-off-by: Dominik <dl6er@dl6er.de>
2026-04-05 20:20:53 +02:00
Dominik cc33fbc545 test: add PUT/DELETE, batch delete, query filter, and auth tests
Add ~80 new pytest API integration tests covering previously untested
endpoints and HTTP methods:

- DELETE 204/404 for groups, domains, clients, lists, config array
  items, network devices, and info messages
- PUT create/replace round-trips for groups, domains, clients, lists
- PUT error cases (missing body, invalid domain type)
- Batch delete (POST :batchDelete) for groups, domains, clients, lists
- DNS blocking toggle (POST disable + re-enable)
- Auth session logout (DELETE /api/auth) and delete-by-ID
- GET endpoints: clients, config, network gateway/routes, info
  host/sensors/metrics, query filters (domain, client_ip, upstream,
  blocklist pseudo-upstream), query suggestions, query cursor
  pagination, stats database with time ranges, history database
  with time ranges
- Search with partial matching
- TOTP credential suggestion (GET /api/auth/totp)
- Config PATCH round-trip (bool and integer, change + verify + restore)
- NTP server protocol-level test (UDP NTPv4 request/response)
- Update test_final.bats known-warning patterns and config write counts

Signed-off-by: Dominik <dl6er@dl6er.de>
2026-03-30 21:26:16 +02:00
Dominik e71ea6d282 Merge branch 'new/pytests' of github.com:pi-hole/FTL into new/pytests 2026-03-29 21:16:51 +02:00
Dominik 1ccc2e6b2e Move blocking test to later in the test suite (to block less time)
Signed-off-by: Dominik <dl6er@dl6er.de>
2026-03-29 21:16:22 +02:00
2437680d40 Update test/test_final.bats
Co-authored-by: Adam Warner <me@adamwarner.co.uk>
Signed-off-by: Dominik <DL6ER@users.noreply.github.com>
2026-03-29 20:59:29 +02:00
Dominik bb89fbfe7b test: add comprehensive API endpoint tests, fix double-free in printFTLenv
Add pytest tests for all previously untested GET API endpoints:
dns/blocking, domains (all type/kind combinations and single lookup),
groups, stats/summary, stats/top_domains, stats/top_clients,
stats/upstreams, stats/query_types, stats/recent_blocked,
stats/database (error handling), dhcp/leases, endpoints, info/ftl,
info/login, info/version, info/messages, info/client, info/database,
info/system, network/devices, network/interfaces, logs (dnsmasq, ftl,
webserver), and padd.

All assertions use exact expected values derived from the deterministic
BATS DNS query seeding (137 total queries, 49 blocked, 47 forwarded,
41 cached, 11 active clients, 8 gravity domains).  On failure, the
full JSON response is dumped to /tmp/ftl_test_*.json for easy
inspection.

Fix double-free bug in printFTLenv() (src/config/env.c): when
printFTLenv() was called more than once (e.g. after config reload
triggered by the CLI password test), it would free item->error a
second time because neither the pointer nor the error_allocated flag
were reset after the first free.  This produced "Trying to free NULL
pointer in printFTLenv()" warnings.  Fix: set item->error = NULL and
item->error_allocated = false after freeing.

Files modified:
  src/config/env.c      — reset error/error_allocated after free
  test/api/test_api.py  — add 34 new endpoint tests (22 -> 56 total)

Signed-off-by: Dominik <dl6er@dl6er.de>
2026-03-25 13:29:17 +01:00
Dominik e189c8e823 test: add pytest API test files and final BATS validation suite
Add the pytest test files and final BATS suite that were described
in the previous commit but not yet included.

New files:
  test/api/conftest.py     — shared fixtures (api_session, openapi, ftl)
  test/api/pytest.ini      — pytest configuration
  test/api/test_api.py     — HTTP errors, config validation, search,
                              history, lists, queries, Lua pages
  test/api/test_openapi.py — OpenAPI spec validation, teleporter
  test/api/test_z_auth.py  — auth workflow (app password, login,
                              rate limiting, password removal)
  test/test_final.bats     — log validation, config rotation counts,
                              FTL termination

Signed-off-by: Dominik <dl6er@dl6er.de>
2026-03-25 11:28:03 +01:00