#!/bin/bash
# autopkgtest for dailyaidecheck.
#
# Runs the installed script against a real aide and a tiny private file tree.
# Everything lives below a temporary directory: the script's directories are
# redirected with LOGDIR/LOCKBASE/TMPBASE, mail is captured by a mock MAILCMD,
# so root is not needed.
#
# No "set -e" on purpose: aide itself exits non-zero (a bitmask 1-7) when it
# finds differences, and a failing check must say what failed and show the
# evidence instead of silently terminating the test.

FAILED=0
CHECKS=0

say()  { printf '%s\n' "$*"; }
pass() { CHECKS=$((CHECKS+1)); say "ok:   $*"; }
fail() { CHECKS=$((CHECKS+1)); FAILED=$((FAILED+1)); say "FAIL: $*"; }

# show <title> <file> [max lines]: print a file indented, for diagnosis
show() {
    printf -- '----- %s -----\n' "$1"
    if [ -s "$2" ]; then
        sed 's/^/    | /' "$2" | head -n "${3:-60}"
    else
        say "    (empty or missing: $2)"
    fi
    say "-----"
}

# evidence: what dailyaidecheck did in its most recent run
evidence() {
    say "    dailyaidecheck exit status: ${DAC_RC:-not run}"
    show "dailyaidecheck stdout" "$T/dac.out" 15
    show "dailyaidecheck stderr" "$T/dac.err" 15
    show "mail captured by the mock MAILCMD" "$MAILOUT" 80
}

finish() {
    say
    if [ "$FAILED" -eq 0 ]; then
        say "PASS: all $CHECKS checks passed"
        exit 0
    fi
    say "FAIL: $FAILED of $CHECKS checks failed"
    exit 1
}

# die: a precondition is not met, nothing after this point would be meaningful
die() { fail "$*"; finish; }

T="$(mktemp -d)" || { say "FAIL: cannot create a temporary directory"; exit 1; }
trap 'rm -rf "$T"' EXIT

# ---------------------------------------------------------------- preflight
AIDE="$(command -v aide)" || die "aide is not installed"
DAC="${DAC:-}"
if [ -z "$DAC" ]; then
    for c in /usr/share/aide/bin/dailyaidecheck /usr/sbin/dailyaidecheck "$(command -v dailyaidecheck)"; do
        if [ -n "$c" ] && [ -x "$c" ]; then DAC="$c"; break; fi
    done
fi
[ -x "$DAC" ] || die "dailyaidecheck not found or not executable (set DAC=/path to override)"
command -v dotlockfile >/dev/null || die "dotlockfile not installed (liblockfile-bin); dailyaidecheck would report an error on every run"
say "aide:           $("$AIDE" --version 2>&1 | head -n 1) ($AIDE)"
say "dailyaidecheck: $DAC"
say "running as uid $(id -u), temp dir $T"
say

# ---------------------------------------------------------------- fixture
mkdir -p "$T"/tree "$T"/db "$T"/log "$T"/lock "$T"/tmp || die "cannot create test directories below $T"
chmod 700 "$T/lock"
echo one > "$T/tree/a"

cat > "$T/aide.conf" <<EOT || die "cannot write $T/aide.conf"
database_in=file:$T/db/aide.db
database_out=file:$T/db/aide.db.new
R = p+u+g+s+sha256
$T/tree R
EOT

# mock for mail(1)/s-nail: record the arguments and the body
MAILOUT="$T/mail.out"
cat > "$T/mail" <<'EOT'
#!/bin/sh
echo "=== MAIL ARGS: $*" >> "$MAILOUT"
cat >> "$MAILOUT"
echo "=== END MAIL" >> "$MAILOUT"
EOT
chmod +x "$T/mail"

export MAILOUT CONFIG="$T/aide.conf" MAILCMD="$T/mail" MAILTO=root FIGLET=no \
       LOGDIR="$T/log" LOCKBASE="$T/lock" TMPBASE="$T/tmp"

# run_aide <init|update|check>: exit codes 0-7 are aide's "what changed" bitmask
run_aide() {
    "$AIDE" --config="$T/aide.conf" "--$1" > "$T/aide.out" 2> "$T/aide.err"
    local rc=$?
    if [ "$rc" -gt 7 ]; then
        fail "aide --$1 failed with exit status $rc (0-7 would be normal)"
        show "aide stdout" "$T/aide.out" 20
        show "aide stderr" "$T/aide.err" 20
        return 1
    fi
    return 0
}

# new_baseline: make the current tree the reference database
new_baseline() {
    run_aide update && mv -f "$T/db/aide.db.new" "$T/db/aide.db"
}

# run_dac [VAR=value ...]: one run of dailyaidecheck, with a fresh mail capture
run_dac() {
    : > "$MAILOUT"
    env "$@" "$DAC" > "$T/dac.out" 2> "$T/dac.err"
    DAC_RC=$?
}

# expectations; each prints one line and, on failure, the evidence
expect_rc() {       # <description> <expected exit status>
    if [ "$DAC_RC" -eq "$2" ]; then pass "$1: exit status $2"
    else fail "$1: exit status was $DAC_RC, expected $2"; evidence; fi
}
expect_in_mail() {  # <description> <fixed string>
    if grep -qF -- "$2" "$MAILOUT"; then pass "$1: mail contains '$2'"
    else fail "$1: mail does not contain '$2'"; evidence; fi
}
expect_not_in_mail() {
    if grep -qF -- "$2" "$MAILOUT"; then fail "$1: mail unexpectedly contains '$2'"; evidence
    else pass "$1: mail does not contain '$2'"; fi
}
expect_mails() {    # <description> <number of mails sent>
    local n; n="$(grep -c '^=== MAIL ARGS' "$MAILOUT")"
    if [ "$n" -eq "$2" ]; then pass "$1: $2 mail(s) sent"
    else fail "$1: $n mail(s) sent, expected $2"; evidence; fi
}
expect_true() {     # <description> <command...>: command must succeed
    local d="$1"; shift
    if "$@"; then pass "$d"; else fail "$d (command failed: $*)"; evidence; fi
}

# ---------------------------------------------------------------- tests
say "== setup: initial database"
if run_aide init && [ -f "$T/db/aide.db.new" ]; then
    mv -f "$T/db/aide.db.new" "$T/db/aide.db"
    pass "aide --init created a database"
else
    show "aide stdout" "$T/aide.out" 20; show "aide stderr" "$T/aide.err" 20
    die "cannot create the initial aide database, the rest of the test is meaningless"
fi

say; say "== 1. unchanged tree"
run_dac
expect_rc        "unchanged tree" 0
expect_mails     "unchanged tree" 1
expect_in_mail   "unchanged tree" "No changes detected"
expect_not_in_mail "unchanged tree" "script errors"
expect_true      "unchanged tree: log written to LOGDIR" test -s "$T/log/aide.log"
expect_true      "unchanged tree: no stray temp dirs left behind in TMPBASE" \
    test -z "$(find "$T/tmp" -mindepth 1 -maxdepth 1 ! -name dailyaidecheck)"

say; say "== 2. changed tree is reported (script exits 0 when aide merely finds changes)"
echo two > "$T/tree/b"
run_dac
expect_rc      "added file" 0
expect_in_mail "added file" "$T/tree/b"

say; say "== 3. NOISE filter (exercises the regexes against this aide version's summary format)"
# tree/b is a real change, tree/noise is the noise: only the latter may disappear
echo noise > "$T/tree/noise"
run_dac NOISE="$T/tree/noise"
expect_rc      "NOISE" 0
expect_in_mail "NOISE" "De-Noised output of the daily AIDE check"
sed -n '/De-Noised output of/,/End of De-Noised/p' "$MAILOUT" > "$T/denoised"
if grep -qF -- "$T/tree/b" "$T/denoised" && ! grep -qF -- "$T/tree/noise" "$T/denoised"; then
    pass "NOISE: de-noised section lists the real change and not the noise"
else
    fail "NOISE: de-noised section should list $T/tree/b but not $T/tree/noise"
    show "de-noised section" "$T/denoised" 20; evidence
fi

say; say "== 4. QUIETREPORTS"
new_baseline
run_dac QUIETREPORTS=yes
expect_rc    "quiet, no changes" 0
expect_mails "quiet, no changes" 0
echo three > "$T/tree/c"
run_dac QUIETREPORTS=yes
expect_mails "quiet, with changes" 1
expect_in_mail "quiet, with changes" "$T/tree/c"

say; say "== 5. COMMAND=update, COPYNEWDB=yes, DAILYAIDECHECK_DEBUG=2 (three runs)"
for i in 1 2 3; do
    echo "run $i" > "$T/tree/c"
    sleep 1                                  # debug copies are named by the second
    run_dac COMMAND=update COPYNEWDB=yes DAILYAIDECHECK_DEBUG=2
    expect_rc "update run $i" 0
done
n="$(find "$T/db" -name 'aide.db.new.debug-*' | wc -l)"
if [ "$n" -eq 2 ]; then pass "debug: exactly 2 copies of the new database kept"
else fail "debug: $n copies kept, expected 2"; ls -l "$T/db" | sed 's/^/    | /'; evidence; fi
expect_true "debug: the newest copy is identical to aide.db.new" \
    cmp -s "$T/db/aide.db.new" "$(find "$T/db" -name 'aide.db.new.debug-*' | sort | tail -n 1)"
expect_true "copy: baseline was replaced by the new database" cmp -s "$T/db/aide.db" "$T/db/aide.db.new"
expect_true "copy: no temporary file left next to the baseline" test ! -e "$T/db/aide.db.dailyaidecheck.tmp"
run_dac
expect_in_mail "check after update" "No changes detected"

say; say "== 6. aide failure must not replace the baseline"
cp -p "$T/db/aide.db" "$T/db/aide.db.saved"
# valid database locations (so that the script gets as far as running aide), then garbage
{ grep '^database_' "$T/aide.conf"; echo "this is not a valid aide configuration"; } > "$T/bad.conf"
echo "run 4" > "$T/tree/c"
run_dac CONFIG="$T/bad.conf" COMMAND=update COPYNEWDB=yes
expect_rc      "aide failure" 1
expect_in_mail "aide failure" "NOT copied"
expect_true    "aide failure: baseline unchanged" cmp -s "$T/db/aide.db" "$T/db/aide.db.saved"

say; say "== 7. invalid DAILYAIDECHECK_DEBUG value is reported"
run_dac COMMAND=update DAILYAIDECHECK_DEBUG=abc
expect_in_mail "invalid debug value" "must be a non-negative integer"

finish
