feat: add e2e container test harness
Implements spec docs/specs/2026-03-30-e2e-container-tests.md: - 5 Containerfiles (ubuntu, debian, fedora, alpine, arch) - test/e2e.sh runner with --runtime, --rebuild, single-distro mode - tmux-based interactive tests (full accept, safety gate decline, signing generate, signing skip) - All scripts pass shellcheck Closes: #18, #19, #20 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
25
test/containers/Containerfile.alpine
Normal file
25
test/containers/Containerfile.alpine
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM alpine:3.21
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
bash \
|
||||||
|
git \
|
||||||
|
openssh-client \
|
||||||
|
openssh-keygen \
|
||||||
|
tmux \
|
||||||
|
coreutils \
|
||||||
|
grep \
|
||||||
|
sed
|
||||||
|
|
||||||
|
RUN adduser -D -s /bin/bash testuser
|
||||||
|
|
||||||
|
COPY git-harden.sh /home/testuser/git-harden.sh
|
||||||
|
COPY test/ /home/testuser/test/
|
||||||
|
RUN chown -R testuser:testuser /home/testuser
|
||||||
|
|
||||||
|
USER testuser
|
||||||
|
WORKDIR /home/testuser
|
||||||
|
|
||||||
|
RUN git config --global user.name "Test User" \
|
||||||
|
&& git config --global user.email "test@example.com"
|
||||||
|
|
||||||
|
CMD ["bash", "test/run.sh"]
|
||||||
22
test/containers/Containerfile.arch
Normal file
22
test/containers/Containerfile.arch
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM archlinux:base
|
||||||
|
|
||||||
|
RUN pacman -Syu --noconfirm \
|
||||||
|
bash \
|
||||||
|
git \
|
||||||
|
openssh \
|
||||||
|
tmux \
|
||||||
|
&& pacman -Scc --noconfirm
|
||||||
|
|
||||||
|
RUN useradd -m -s /bin/bash testuser
|
||||||
|
|
||||||
|
COPY git-harden.sh /home/testuser/git-harden.sh
|
||||||
|
COPY test/ /home/testuser/test/
|
||||||
|
RUN chown -R testuser:testuser /home/testuser
|
||||||
|
|
||||||
|
USER testuser
|
||||||
|
WORKDIR /home/testuser
|
||||||
|
|
||||||
|
RUN git config --global user.name "Test User" \
|
||||||
|
&& git config --global user.email "test@example.com"
|
||||||
|
|
||||||
|
CMD ["bash", "test/run.sh"]
|
||||||
22
test/containers/Containerfile.debian
Normal file
22
test/containers/Containerfile.debian
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM debian:trixie
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
bash \
|
||||||
|
git \
|
||||||
|
openssh-client \
|
||||||
|
tmux \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN useradd -m -s /bin/bash testuser
|
||||||
|
|
||||||
|
COPY git-harden.sh /home/testuser/git-harden.sh
|
||||||
|
COPY test/ /home/testuser/test/
|
||||||
|
RUN chown -R testuser:testuser /home/testuser
|
||||||
|
|
||||||
|
USER testuser
|
||||||
|
WORKDIR /home/testuser
|
||||||
|
|
||||||
|
RUN git config --global user.name "Test User" \
|
||||||
|
&& git config --global user.email "test@example.com"
|
||||||
|
|
||||||
|
CMD ["bash", "test/run.sh"]
|
||||||
22
test/containers/Containerfile.fedora
Normal file
22
test/containers/Containerfile.fedora
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM fedora:42
|
||||||
|
|
||||||
|
RUN dnf install -y \
|
||||||
|
bash \
|
||||||
|
git \
|
||||||
|
openssh-clients \
|
||||||
|
tmux \
|
||||||
|
&& dnf clean all
|
||||||
|
|
||||||
|
RUN useradd -m -s /bin/bash testuser
|
||||||
|
|
||||||
|
COPY git-harden.sh /home/testuser/git-harden.sh
|
||||||
|
COPY test/ /home/testuser/test/
|
||||||
|
RUN chown -R testuser:testuser /home/testuser
|
||||||
|
|
||||||
|
USER testuser
|
||||||
|
WORKDIR /home/testuser
|
||||||
|
|
||||||
|
RUN git config --global user.name "Test User" \
|
||||||
|
&& git config --global user.email "test@example.com"
|
||||||
|
|
||||||
|
CMD ["bash", "test/run.sh"]
|
||||||
22
test/containers/Containerfile.ubuntu
Normal file
22
test/containers/Containerfile.ubuntu
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
bash \
|
||||||
|
git \
|
||||||
|
openssh-client \
|
||||||
|
tmux \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN useradd -m -s /bin/bash testuser
|
||||||
|
|
||||||
|
COPY git-harden.sh /home/testuser/git-harden.sh
|
||||||
|
COPY test/ /home/testuser/test/
|
||||||
|
RUN chown -R testuser:testuser /home/testuser
|
||||||
|
|
||||||
|
USER testuser
|
||||||
|
WORKDIR /home/testuser
|
||||||
|
|
||||||
|
RUN git config --global user.name "Test User" \
|
||||||
|
&& git config --global user.email "test@example.com"
|
||||||
|
|
||||||
|
CMD ["bash", "test/run.sh"]
|
||||||
292
test/e2e.sh
Executable file
292
test/e2e.sh
Executable file
@@ -0,0 +1,292 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# test/e2e.sh — Run BATS tests inside containers across Linux distros
|
||||||
|
# Usage: test/e2e.sh [--runtime docker|podman] [--rebuild] [distro]
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Constants
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
readonly SCRIPT_DIR
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||||
|
readonly REPO_ROOT
|
||||||
|
readonly CONTAINER_DIR="${SCRIPT_DIR}/containers"
|
||||||
|
readonly IMAGE_PREFIX="git-harden-test"
|
||||||
|
|
||||||
|
# Distro matrix: name=image
|
||||||
|
readonly DISTROS="ubuntu debian fedora alpine arch"
|
||||||
|
|
||||||
|
# Colors (empty if not a terminal)
|
||||||
|
if [ -t 2 ]; then
|
||||||
|
readonly C_RED='\033[0;31m'
|
||||||
|
readonly C_GREEN='\033[0;32m'
|
||||||
|
readonly C_YELLOW='\033[0;33m'
|
||||||
|
readonly C_BOLD='\033[1m'
|
||||||
|
readonly C_RESET='\033[0m'
|
||||||
|
else
|
||||||
|
readonly C_RED=''
|
||||||
|
readonly C_GREEN=''
|
||||||
|
readonly C_YELLOW=''
|
||||||
|
readonly C_BOLD=''
|
||||||
|
readonly C_RESET=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Mutable state
|
||||||
|
RUNTIME=""
|
||||||
|
REBUILD=false
|
||||||
|
TARGET_DISTRO=""
|
||||||
|
|
||||||
|
# Results tracking
|
||||||
|
RESULTS_DISTROS=""
|
||||||
|
RESULTS_STATUS=""
|
||||||
|
RESULTS_DURATION=""
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf '%bError:%b %s\n' "$C_RED" "$C_RESET" "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
info() {
|
||||||
|
printf '%b[INFO]%b %s\n' "$C_YELLOW" "$C_RESET" "$1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Argument parsing
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
parse_args() {
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--runtime)
|
||||||
|
[ $# -ge 2 ] || die "--runtime requires an argument (docker or podman)"
|
||||||
|
RUNTIME="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--rebuild)
|
||||||
|
REBUILD=true
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
die "Unknown option: $1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
TARGET_DISTRO="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat >&2 <<'EOF'
|
||||||
|
Usage: test/e2e.sh [OPTIONS] [DISTRO]
|
||||||
|
|
||||||
|
Run BATS tests inside containers across Linux distributions.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--runtime <docker|podman> Container runtime (auto-detected if omitted)
|
||||||
|
--rebuild Force image rebuild ignoring cache
|
||||||
|
--help, -h Show this help
|
||||||
|
|
||||||
|
Distros: ubuntu, debian, fedora, alpine, arch
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
test/e2e.sh # Run all distros
|
||||||
|
test/e2e.sh alpine # Run Alpine only
|
||||||
|
test/e2e.sh --runtime podman fedora # Use Podman, Fedora only
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Runtime detection
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
detect_runtime() {
|
||||||
|
if [ -n "$RUNTIME" ]; then
|
||||||
|
if ! command -v "$RUNTIME" >/dev/null 2>&1; then
|
||||||
|
die "${RUNTIME} is not installed"
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local has_docker=false
|
||||||
|
local has_podman=false
|
||||||
|
|
||||||
|
if command -v docker >/dev/null 2>&1; then
|
||||||
|
has_docker=true
|
||||||
|
fi
|
||||||
|
if command -v podman >/dev/null 2>&1; then
|
||||||
|
has_podman=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$has_docker" = true ]; then
|
||||||
|
RUNTIME="docker"
|
||||||
|
elif [ "$has_podman" = true ]; then
|
||||||
|
RUNTIME="podman"
|
||||||
|
else
|
||||||
|
die "Neither docker nor podman found. Install one of them:\n brew install docker\n brew install podman"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify the daemon is running
|
||||||
|
if ! "$RUNTIME" info >/dev/null 2>&1; then
|
||||||
|
die "${RUNTIME} daemon is not running. Is the ${RUNTIME} service started?"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Build & run
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
build_image() {
|
||||||
|
local distro="$1"
|
||||||
|
local containerfile="${CONTAINER_DIR}/Containerfile.${distro}"
|
||||||
|
local image_name="${IMAGE_PREFIX}:${distro}"
|
||||||
|
|
||||||
|
if [ ! -f "$containerfile" ]; then
|
||||||
|
die "Containerfile not found: $containerfile"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local build_args=()
|
||||||
|
if [ "$REBUILD" = true ]; then
|
||||||
|
build_args+=("--no-cache")
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Building ${image_name}..."
|
||||||
|
if ! "$RUNTIME" build \
|
||||||
|
"${build_args[@]}" \
|
||||||
|
-f "$containerfile" \
|
||||||
|
-t "$image_name" \
|
||||||
|
"$REPO_ROOT" 2>&1; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_tests() {
|
||||||
|
local distro="$1"
|
||||||
|
local image_name="${IMAGE_PREFIX}:${distro}"
|
||||||
|
|
||||||
|
info "Running tests on ${distro}..."
|
||||||
|
if ! "$RUNTIME" run \
|
||||||
|
--rm \
|
||||||
|
--network=none \
|
||||||
|
"$image_name" \
|
||||||
|
bash test/run.sh 2>&1; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_distro() {
|
||||||
|
local distro="$1"
|
||||||
|
local start_time
|
||||||
|
start_time="$(date +%s)"
|
||||||
|
|
||||||
|
printf '\n%b══ %s ══%b\n' "$C_BOLD" "$distro" "$C_RESET" >&2
|
||||||
|
|
||||||
|
local status="PASS"
|
||||||
|
|
||||||
|
if ! build_image "$distro"; then
|
||||||
|
status="FAIL (build)"
|
||||||
|
elif ! run_tests "$distro"; then
|
||||||
|
status="FAIL (tests)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local end_time
|
||||||
|
end_time="$(date +%s)"
|
||||||
|
local duration=$(( end_time - start_time ))
|
||||||
|
|
||||||
|
# Append to results
|
||||||
|
RESULTS_DISTROS="${RESULTS_DISTROS}${distro}\n"
|
||||||
|
RESULTS_STATUS="${RESULTS_STATUS}${status}\n"
|
||||||
|
RESULTS_DURATION="${RESULTS_DURATION}${duration}s\n"
|
||||||
|
|
||||||
|
if [ "$status" = "PASS" ]; then
|
||||||
|
printf '%b ✓ %s passed (%ds)%b\n' "$C_GREEN" "$distro" "$duration" "$C_RESET" >&2
|
||||||
|
else
|
||||||
|
printf '%b ✗ %s %s (%ds)%b\n' "$C_RED" "$distro" "$status" "$duration" "$C_RESET" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Summary
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
print_summary() {
|
||||||
|
printf '\n%b══ Summary ══%b\n' "$C_BOLD" "$C_RESET" >&2
|
||||||
|
printf '%-12s %-20s %s\n' "DISTRO" "STATUS" "DURATION" >&2
|
||||||
|
printf '%-12s %-20s %s\n' "------" "------" "--------" >&2
|
||||||
|
|
||||||
|
local distros_arr status_arr duration_arr
|
||||||
|
IFS=$'\n' read -r -d '' -a distros_arr <<< "$(printf '%b' "$RESULTS_DISTROS")" || true
|
||||||
|
IFS=$'\n' read -r -d '' -a status_arr <<< "$(printf '%b' "$RESULTS_STATUS")" || true
|
||||||
|
IFS=$'\n' read -r -d '' -a duration_arr <<< "$(printf '%b' "$RESULTS_DURATION")" || true
|
||||||
|
|
||||||
|
local i=0
|
||||||
|
local any_failed=false
|
||||||
|
while [ "$i" -lt "${#distros_arr[@]}" ]; do
|
||||||
|
local d="${distros_arr[$i]}"
|
||||||
|
local s="${status_arr[$i]}"
|
||||||
|
local t="${duration_arr[$i]}"
|
||||||
|
[ -z "$d" ] && { i=$((i + 1)); continue; }
|
||||||
|
|
||||||
|
local color="$C_GREEN"
|
||||||
|
if [ "$s" != "PASS" ]; then
|
||||||
|
color="$C_RED"
|
||||||
|
any_failed=true
|
||||||
|
fi
|
||||||
|
printf '%b%-12s %-20s %s%b\n' "$color" "$d" "$s" "$t" "$C_RESET" >&2
|
||||||
|
i=$((i + 1))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$any_failed" = true ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
main() {
|
||||||
|
parse_args "$@"
|
||||||
|
detect_runtime
|
||||||
|
|
||||||
|
info "Using runtime: ${RUNTIME}"
|
||||||
|
|
||||||
|
if [ -n "$TARGET_DISTRO" ]; then
|
||||||
|
# Validate distro name
|
||||||
|
local valid=false
|
||||||
|
for d in $DISTROS; do
|
||||||
|
if [ "$d" = "$TARGET_DISTRO" ]; then
|
||||||
|
valid=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ "$valid" = false ]; then
|
||||||
|
die "Unknown distro: ${TARGET_DISTRO}. Available: ${DISTROS}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_distro "$TARGET_DISTRO"
|
||||||
|
else
|
||||||
|
for d in $DISTROS; do
|
||||||
|
run_distro "$d"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_summary
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
91
test/interactive/helpers.sh
Executable file
91
test/interactive/helpers.sh
Executable file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Shared helpers for interactive tmux-driven tests
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
readonly TMUX_SESSION="test"
|
||||||
|
readonly SCRIPT_PATH="${HOME}/git-harden.sh"
|
||||||
|
|
||||||
|
# Colors
|
||||||
|
if [ -t 2 ]; then
|
||||||
|
readonly C_RED='\033[0;31m'
|
||||||
|
readonly C_GREEN='\033[0;32m'
|
||||||
|
readonly C_RESET='\033[0m'
|
||||||
|
else
|
||||||
|
readonly C_RED=''
|
||||||
|
readonly C_GREEN=''
|
||||||
|
readonly C_RESET=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Wait for a string to appear in the tmux pane.
|
||||||
|
# Polls every 0.2s, times out after $2 seconds (default 10).
|
||||||
|
wait_for() {
|
||||||
|
local pattern="$1"
|
||||||
|
local timeout="${2:-10}"
|
||||||
|
local elapsed=0
|
||||||
|
while ! tmux capture-pane -t "$TMUX_SESSION" -p | grep -qF "$pattern"; do
|
||||||
|
sleep 0.2
|
||||||
|
elapsed=$(( elapsed + 1 ))
|
||||||
|
if (( elapsed > timeout * 5 )); then
|
||||||
|
printf 'TIMEOUT waiting for: %s\n' "$pattern" >&2
|
||||||
|
printf 'Current pane content:\n' >&2
|
||||||
|
tmux capture-pane -t "$TMUX_SESSION" -p >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send keys to the tmux session
|
||||||
|
send() {
|
||||||
|
tmux send-keys -t "$TMUX_SESSION" "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Start git-harden.sh in a tmux session
|
||||||
|
start_session() {
|
||||||
|
tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true
|
||||||
|
tmux new-session -d -s "$TMUX_SESSION" "bash ${SCRIPT_PATH}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Wait for the script to exit and capture final output
|
||||||
|
capture_output() {
|
||||||
|
# Wait for the shell to become available (script exited)
|
||||||
|
local timeout=30
|
||||||
|
local elapsed=0
|
||||||
|
while tmux list-panes -t "$TMUX_SESSION" -F '#{pane_dead}' 2>/dev/null | grep -q '^0$'; do
|
||||||
|
sleep 0.5
|
||||||
|
elapsed=$(( elapsed + 1 ))
|
||||||
|
if (( elapsed > timeout * 2 )); then
|
||||||
|
printf 'TIMEOUT waiting for script to exit\n' >&2
|
||||||
|
tmux capture-pane -t "$TMUX_SESSION" -p >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
tmux capture-pane -t "$TMUX_SESSION" -p
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
cleanup() {
|
||||||
|
tmux kill-session -t "$TMUX_SESSION" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# Assert helper
|
||||||
|
assert_contains() {
|
||||||
|
local haystack="$1"
|
||||||
|
local needle="$2"
|
||||||
|
if printf '%s' "$haystack" | grep -qF "$needle"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
printf '%bFAIL:%b expected output to contain: %s\n' "$C_RED" "$C_RESET" "$needle" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
pass() {
|
||||||
|
printf '%b PASS:%b %s\n' "$C_GREEN" "$C_RESET" "$1" >&2
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
printf '%b FAIL:%b %s\n' "$C_RED" "$C_RESET" "$1" >&2
|
||||||
|
}
|
||||||
30
test/interactive/run-all.sh
Executable file
30
test/interactive/run-all.sh
Executable file
@@ -0,0 +1,30 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Run all interactive tmux-driven tests
|
||||||
|
# Intended to be run inside a container (with tmux installed)
|
||||||
|
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
passed=0
|
||||||
|
failed=0
|
||||||
|
total=0
|
||||||
|
|
||||||
|
for test_script in "${SCRIPT_DIR}"/test-*.sh; do
|
||||||
|
[ -f "$test_script" ] || continue
|
||||||
|
total=$((total + 1))
|
||||||
|
printf '\n── %s ──\n' "$(basename "$test_script")" >&2
|
||||||
|
if bash "$test_script"; then
|
||||||
|
passed=$((passed + 1))
|
||||||
|
else
|
||||||
|
failed=$((failed + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf '\n── Interactive tests: %d passed, %d failed, %d total ──\n' "$passed" "$failed" "$total" >&2
|
||||||
|
|
||||||
|
if [ "$failed" -gt 0 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
60
test/interactive/test-full-accept.sh
Executable file
60
test/interactive/test-full-accept.sh
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Interactive test: accept all prompts (safety gate + hardening + signing skip)
|
||||||
|
# Verifies: all settings applied, re-audit exits 0
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck source=helpers.sh
|
||||||
|
source "${SCRIPT_DIR}/helpers.sh"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
printf 'Test: Full interactive apply (accept all)\n' >&2
|
||||||
|
|
||||||
|
start_session
|
||||||
|
|
||||||
|
# Safety review gate — answer yes
|
||||||
|
wait_for "reviewed this script"
|
||||||
|
send "y" Enter
|
||||||
|
|
||||||
|
# Proceed with hardening — answer yes
|
||||||
|
wait_for "Proceed with hardening"
|
||||||
|
send "y" Enter
|
||||||
|
|
||||||
|
# Accept each setting prompt by sending "y" + Enter repeatedly
|
||||||
|
local pane_content
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
sleep 0.3
|
||||||
|
pane_content="$(tmux capture-pane -t "$TMUX_SESSION" -p 2>/dev/null || true)"
|
||||||
|
if printf '%s' "$pane_content" | grep -qF "Signing key options"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if printf '%s' "$pane_content" | grep -qF "Hardening complete"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
send "y" Enter
|
||||||
|
done
|
||||||
|
|
||||||
|
# Signing wizard — skip
|
||||||
|
wait_for "Signing key options" 15
|
||||||
|
send "s" Enter
|
||||||
|
|
||||||
|
# Wait for completion
|
||||||
|
sleep 2
|
||||||
|
capture_output >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Verify: re-run audit — signing won't pass (skipped) but git config should
|
||||||
|
if git config --global --get transfer.fsckObjects | grep -q true; then
|
||||||
|
pass "Full accept: git config settings applied (signing skipped as expected)"
|
||||||
|
else
|
||||||
|
fail "Full accept: settings not applied"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
49
test/interactive/test-safety-gate-decline.sh
Executable file
49
test/interactive/test-safety-gate-decline.sh
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Interactive test: decline safety review gate
|
||||||
|
# Verifies: script exits 0, prints AI review instructions, no config changes
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck source=helpers.sh
|
||||||
|
source "${SCRIPT_DIR}/helpers.sh"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
printf 'Test: Safety gate decline\n' >&2
|
||||||
|
|
||||||
|
# Snapshot current config
|
||||||
|
local config_before
|
||||||
|
config_before="$(git config --global --list 2>/dev/null || true)"
|
||||||
|
|
||||||
|
start_session
|
||||||
|
|
||||||
|
# Safety review gate — answer no (default)
|
||||||
|
wait_for "reviewed this script"
|
||||||
|
send "n" Enter
|
||||||
|
|
||||||
|
# Wait for exit
|
||||||
|
sleep 2
|
||||||
|
local output
|
||||||
|
output="$(capture_output)"
|
||||||
|
|
||||||
|
# Verify: output contains AI review instructions
|
||||||
|
assert_contains "$output" "claude"
|
||||||
|
assert_contains "$output" "gemini"
|
||||||
|
|
||||||
|
# Verify: no config changes
|
||||||
|
local config_after
|
||||||
|
config_after="$(git config --global --list 2>/dev/null || true)"
|
||||||
|
if [ "$config_before" = "$config_after" ]; then
|
||||||
|
pass "Safety gate decline: no config changes, instructions shown"
|
||||||
|
else
|
||||||
|
fail "Safety gate decline: config was modified"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
89
test/interactive/test-signing-generate.sh
Executable file
89
test/interactive/test-signing-generate.sh
Executable file
@@ -0,0 +1,89 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Interactive test: generate ed25519 key via signing wizard
|
||||||
|
# Verifies: key created, user.signingkey configured, commit.gpgsign=true
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck source=helpers.sh
|
||||||
|
source "${SCRIPT_DIR}/helpers.sh"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
printf 'Test: Signing wizard - generate ed25519 key\n' >&2
|
||||||
|
|
||||||
|
# Ensure no existing keys
|
||||||
|
rm -f "${HOME}/.ssh/id_ed25519" "${HOME}/.ssh/id_ed25519.pub"
|
||||||
|
|
||||||
|
start_session
|
||||||
|
|
||||||
|
# Safety review gate
|
||||||
|
wait_for "reviewed this script"
|
||||||
|
send "y" Enter
|
||||||
|
|
||||||
|
# Proceed with hardening
|
||||||
|
wait_for "Proceed with hardening"
|
||||||
|
send "y" Enter
|
||||||
|
|
||||||
|
# Accept settings until signing wizard
|
||||||
|
local pane_content
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
sleep 0.3
|
||||||
|
pane_content="$(tmux capture-pane -t "$TMUX_SESSION" -p 2>/dev/null || true)"
|
||||||
|
if printf '%s' "$pane_content" | grep -qF "Signing key options"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if printf '%s' "$pane_content" | grep -qF "Hardening complete"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
send "y" Enter
|
||||||
|
done
|
||||||
|
|
||||||
|
# Signing wizard — option 1: generate ed25519
|
||||||
|
wait_for "Signing key options" 15
|
||||||
|
send "1" Enter
|
||||||
|
|
||||||
|
# ssh-keygen prompts for passphrase — enter empty twice
|
||||||
|
wait_for "Enter passphrase" 10
|
||||||
|
send "" Enter
|
||||||
|
wait_for "Enter same passphrase" 10
|
||||||
|
send "" Enter
|
||||||
|
|
||||||
|
# Wait for completion
|
||||||
|
sleep 3
|
||||||
|
capture_output >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Verify key exists
|
||||||
|
if [ -f "${HOME}/.ssh/id_ed25519.pub" ]; then
|
||||||
|
pass "Key generated: ~/.ssh/id_ed25519.pub exists"
|
||||||
|
else
|
||||||
|
fail "Key not generated"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify signing key configured
|
||||||
|
local signing_key
|
||||||
|
signing_key="$(git config --global --get user.signingkey 2>/dev/null || true)"
|
||||||
|
if [ -n "$signing_key" ]; then
|
||||||
|
pass "user.signingkey configured: ${signing_key}"
|
||||||
|
else
|
||||||
|
fail "user.signingkey not configured"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify gpgsign enabled
|
||||||
|
local gpgsign
|
||||||
|
gpgsign="$(git config --global --get commit.gpgsign 2>/dev/null || true)"
|
||||||
|
if [ "$gpgsign" = "true" ]; then
|
||||||
|
pass "commit.gpgsign=true"
|
||||||
|
else
|
||||||
|
fail "commit.gpgsign not set"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
72
test/interactive/test-signing-skip.sh
Executable file
72
test/interactive/test-signing-skip.sh
Executable file
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Interactive test: skip signing wizard
|
||||||
|
# Verifies: no signing key configured, commit.gpgsign not set
|
||||||
|
|
||||||
|
set -o errexit
|
||||||
|
set -o nounset
|
||||||
|
set -o pipefail
|
||||||
|
IFS=$'\n\t'
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
# shellcheck source=helpers.sh
|
||||||
|
source "${SCRIPT_DIR}/helpers.sh"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
printf 'Test: Signing wizard - skip\n' >&2
|
||||||
|
|
||||||
|
start_session
|
||||||
|
|
||||||
|
# Safety review gate
|
||||||
|
wait_for "reviewed this script"
|
||||||
|
send "y" Enter
|
||||||
|
|
||||||
|
# Proceed with hardening
|
||||||
|
wait_for "Proceed with hardening"
|
||||||
|
send "y" Enter
|
||||||
|
|
||||||
|
# Accept settings until signing wizard
|
||||||
|
local pane_content
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
sleep 0.3
|
||||||
|
pane_content="$(tmux capture-pane -t "$TMUX_SESSION" -p 2>/dev/null || true)"
|
||||||
|
if printf '%s' "$pane_content" | grep -qF "Signing key options"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if printf '%s' "$pane_content" | grep -qF "Hardening complete"; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
send "y" Enter
|
||||||
|
done
|
||||||
|
|
||||||
|
# Signing wizard — skip
|
||||||
|
wait_for "Signing key options" 15
|
||||||
|
send "s" Enter
|
||||||
|
|
||||||
|
# Wait for completion
|
||||||
|
sleep 2
|
||||||
|
capture_output >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# Verify: no signing key
|
||||||
|
local signing_key
|
||||||
|
signing_key="$(git config --global --get user.signingkey 2>/dev/null || true)"
|
||||||
|
if [ -z "$signing_key" ]; then
|
||||||
|
pass "Signing skip: user.signingkey not set"
|
||||||
|
else
|
||||||
|
fail "Signing skip: user.signingkey was set unexpectedly: ${signing_key}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verify: commit.gpgsign not set
|
||||||
|
local gpgsign
|
||||||
|
gpgsign="$(git config --global --get commit.gpgsign 2>/dev/null || true)"
|
||||||
|
if [ -z "$gpgsign" ]; then
|
||||||
|
pass "Signing skip: commit.gpgsign not set"
|
||||||
|
else
|
||||||
|
fail "Signing skip: commit.gpgsign was set unexpectedly: ${gpgsign}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
Reference in New Issue
Block a user