#!/usr/bin/env bash # # tests/run-tests.sh — doorman decoder unit tests (offline, no hardware). # # Builds synthetic /dev/input event streams (the exact binary records the # kernel emits) and asserts bin/doorman.sh --selftest decodes them the way # the 2018 Perl doorman did: digits 1-9 = HID usages 30-38, 0 = 39, # Enter = 40, each key arriving as a down/up MSC_SCAN pair that the # parity logic collapses to one digit. # # Usage: bash tests/run-tests.sh # set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BIN="$ROOT/bin/doorman.sh" TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT pass=0 fail=0 ok() { printf 'ok - %s\n' "$1"; pass=$((pass + 1)); } no() { printf 'FAIL - %s\n' "$1"; fail=$((fail + 1)); } # --------------------------------------------------------------------------- # Fixture builders # --------------------------------------------------------------------------- hex() { printf '\\x%02x' "$1"; } # emit_event — one input_event record, LE. emit_event() { local w="$1" t="$2" c="$3" v="$4" tv if [ "$w" -eq 24 ]; then tv=16; else tv=8; fi # shellcheck disable=SC2046 # %.0s consumes the seq args; emits tv NULs printf '\x00%.0s' $(seq 1 "$tv") > "$TMP/ev.bin" { # type and code are __u16 LE, value is __s32 LE printf '%b' "$(hex $((t & 255)))$(hex $(((t >> 8) & 255)))$(hex $((c & 255)))$(hex $(((c >> 8) & 255)))" printf '%b' "$(hex $((v & 255)))$(hex $(((v >> 8) & 255)))$(hex $(((v >> 16) & 255)))$(hex $(((v >> 24) & 255)))" } >> "$TMP/ev.bin" cat "$TMP/ev.bin" } # emit_scan — one keypress: down + up MSC_SCAN + SYN filler. emit_scan() { emit_event "$1" 4 4 "$2" emit_event "$1" 4 4 "$2" emit_event "$1" 0 0 0 } # emit_badge — each digit + terminating Enter. emit_badge() { local w="$1"; shift local d u for d in "$@"; do if [ "$d" = "ENTER" ]; then u=40; else u=$((29 + 10#$d)); fi emit_scan "$w" "$u" done } # emit_digits — one scan event pair per digit (0 = usage 39, # 1-9 = usages 30-38). Takes the badge string itself, not precomputed usages. emit_digits() { local w="$1" s="$2" i d u for ((i = 0; i < ${#s}; i++)); do d="${s:$i:1}" if [ "$d" = "0" ]; then u=39; else u=$((29 + 10#$d)); fi emit_scan "$w" "$u" done } # emit_enter — the Enter terminator keypress. emit_enter() { emit_scan "$1" 40; } run_selftest() { # run_selftest [width] local w="${2:-24}" "$BIN" --selftest "$1" --width "$w" 2>/dev/null } assert_ids() { # assert_ids [--width N] local desc="$1" fixture="$2" shift 2 local w=24 local -a exp=() while [ $# -gt 0 ]; do if [ "$1" = "--width" ]; then w="$2" shift 2 else exp+=("$1") shift fi done local got expected got="$(run_selftest "$fixture" "$w")" expected="$(printf '%s\n' "${exp[@]}")" if [ "$got" = "$expected" ]; then ok "$desc" else no "$desc" printf ' expected: %s\n' "$(printf '%s | ' "${exp[@]}")" printf ' got: %s\n' "$(printf '%s' "$got" | tr '\n' ' ')" fi } # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- # T1: two full 10-digit badges + a repeated-digit badge ("11" exercises the # down/up parity state machine on consecutive identical scan codes). f1="$TMP/f1.bin" { emit_digits 24 "0000000001"; emit_enter 24 emit_digits 24 "0000000002"; emit_enter 24 emit_digits 24 "11"; emit_enter 24 } > "$f1" assert_ids "T1 decodes two 10-digit badges + repeated-digit badge" "$f1" \ "0000000001" "0000000002" "11" # T2: garbage MSC_SCAN usage mid-badge (e.g. media key) must not corrupt # or truncate the scan (2018 behavior: log+skip, id preserved). f2="$TMP/f2.bin" { emit_digits 24 "000" emit_scan 24 42 # garbage usage (backspace) — must be skipped emit_digits 24 "0000001" emit_enter 24 } > "$f2" assert_ids "T2 garbage scan usage mid-badge is skipped" "$f2" "0000000001" # T3: lone Enter (empty scan) emits nothing. f3="$TMP/f3.bin" emit_enter 24 > "$f3" got="$(run_selftest "$f3")" if [ -z "$got" ]; then ok "T3 lone Enter emits no badge"; else no "T3 lone Enter emits no badge (got: $got)"; fi # T4: non-MSC events (EV_KEY type 1) around a badge are ignored. f4="$TMP/f4.bin" { emit_event 24 1 30 1; emit_event 24 1 30 0 emit_digits 24 "0"; emit_enter 24 emit_event 24 1 30 1; emit_event 24 1 30 0 } > "$f4" assert_ids "T4 EV_KEY events ignored, MSC_SCAN decoded" "$f4" "0" # T5: truncated trailing record does not hang or corrupt output. f5="$TMP/f5.bin" { emit_digits 24 "7"; emit_enter 24 emit_event 24 4 4 39 printf '\x00\x00\x00' # 3-byte orphan tail } > "$f5" assert_ids "T5 truncated tail record ignored" "$f5" "7" # T6: 16-byte records (32-bit boards, e.g. older Pis) decode identically. f6="$TMP/f6.bin" { emit_digits 16 "0"; emit_enter 16; } > "$f6" assert_ids "T6 16-byte record width supported" "$f6" "0" --width 16 # T7: all-zero stream (idle bus) emits nothing. f7="$TMP/f7.bin" for _ in 1 2 3 4 5; do emit_event 24 0 0 0; done > "$f7" got="$(run_selftest "$f7")" if [ -z "$got" ]; then ok "T7 idle all-zero stream emits nothing"; else no "T7 idle stream (got: $got)"; fi # T8: missing fixture file fails cleanly, not silently. if "$BIN" --selftest "$TMP/does-not-exist.bin" >/dev/null 2>&1; then no "T8 missing fixture exits nonzero" else ok "T8 missing fixture exits nonzero" fi # T9: known 2018 backdoor badges replay end-to-end (regression anchor). f9="$TMP/f9.bin" { emit_digits 24 "0000000001"; emit_enter 24 emit_digits 24 "0000000002"; emit_enter 24 } > "$f9" assert_ids "T9 2018 backdoor badges decode verbatim" "$f9" \ "0000000001" "0000000002" # --------------------------------------------------------------------------- printf '\n%d passed, %d failed\n' "$pass" "$fail" [ "$fail" -eq 0 ]