#!/usr/bin/env bash # Проверка deploy/backup.sh без docker: синтетический инстанс во временном # каталоге, локальный offsite-приёмник (rsync) и поведение при недоступной # выгрузке. Запуск: make backup-test (AGENT.md 10.8, 11.3). set -euo pipefail REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" WORK="$(mktemp -d "${TMPDIR:-/tmp}/glchat-backup-test.XXXXXX")" trap 'rm -rf "$WORK"' EXIT checks=0 errors=0 ok() { checks=$((checks + 1)) printf 'ok %s\n' "$*" } fail() { checks=$((checks + 1)) errors=$((errors + 1)) printf 'ОШИБКА %s\n' "$*" >&2 } expect() { # expect <описание> <ожидаемое> <фактическое> if [ "$2" = "$3" ]; then ok "$1 = $3" else fail "$1: ожидалось '$2', получено '$3'" fi } # --- синтетический инстанс ------------------------------------------------- ETC="${WORK}/etc" DATA="${ETC}/data" mkdir -p "${DATA}/db" "${DATA}/files/attachments" "${ETC}/livekit" sqlite3 "${DATA}/db/glchat.db" <<'SQL' CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT NOT NULL); CREATE TABLE messages (id INTEGER PRIMARY KEY, body TEXT NOT NULL); INSERT INTO users (email) VALUES ('admin@example.test'), ('guest@example.test'); INSERT INTO messages (body) VALUES ('привет'), ('как дела'); SQL printf 'файл вложения' >"${DATA}/files/attachments/a.txt" printf 'service: glchat\n' >"${ETC}/compose.yaml" printf 'port: 7880\n' >"${ETC}/livekit/livekit.yaml" cat >"${ETC}/.env" <<'ENV' DOMAIN=gl.example.test APP_VERSION=v0.0.0-test PROFILE=standard BACKUP_KEEP_DAYS=30 ENV run_backup() { # run_backup [keep-days] BACKUP_OFFSITE_TARGET="$1" BACKUP_OFFSITE_KEEP_DAYS="${2:-}" \ GLCHAT_ETC_DIR="$ETC" "${REPO_DIR}/deploy/backup.sh" } # --- 1. без offsite -------------------------------------------------------- log_out="$(run_backup "")" expect "локальный бэкап создан" "1" "$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')" case "$log_out" in *"offsite"*) fail "offsite упомянут, хотя цель не задана" ;; *) ok "без BACKUP_OFFSITE_TARGET выгрузки нет" ;; esac # --- 2. offsite в локальный каталог (rsync) ------------------------------- OFFSITE="${WORK}/offsite" mkdir -p "$OFFSITE" run_backup "$OFFSITE" >/dev/null expect "offsite-копия доехала" "1" "$(find "$OFFSITE" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')" local_newest="$(ls -t "${DATA}/backups"/glchat-*.tar.gz | head -1)" local_base="$(basename "$local_newest")" if [ -f "${OFFSITE}/${local_base}" ] && cmp -s "$local_newest" "${OFFSITE}/${local_base}"; then ok "offsite-копия ${local_base} побайтово совпадает с локальной" else fail "offsite-копия ${local_base} отсутствует или отличается" fi # --- 3. недоступный приёмник: предупреждение, но не авария ---------------- before="$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')" marker="${WORK}/marker" touch "$marker" if log_out="$(run_backup "no-such-host.invalid:/nonexistent" 2>&1)"; then ok "недоступный offsite не роняет бэкап (код 0)" else fail "недоступный offsite привёл к ошибке" fi case "$log_out" in *"offsite-копия не отправлена"*) ok "предупреждение о неудачной выгрузке есть" ;; *) fail "нет предупреждения о неудачной выгрузке" ;; esac after="$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')" expect "локальные бэкапы продолжают создаваться" "$((before + 1))" "$after" # --- 4. нечисловой BACKUP_OFFSITE_KEEP_DAYS отключает ротацию ------------- if log_out="$(run_backup "$OFFSITE" "пять" 2>&1)"; then ok "нечисловая offsite-ротация не роняет бэкап" else fail "нечисловая offsite-ротация привела к ошибке" fi case "$log_out" in *"не число"*) ok "предупреждение о нечисловом значении есть" ;; *) fail "нет предупреждения о нечисловом значении" ;; esac # --- 5. AGE_RECIPIENT: шифрование при наличии age, внятная ошибка без него -- # AGE_RECIPIENT дописываем во временную копию .env и возвращаем исходный файл: # дальше проверяется offsite через rclone, где шифрование только мешает. cp "${ETC}/.env" "${ETC}/env.keep" if command -v age >/dev/null 2>&1 && command -v age-keygen >/dev/null 2>&1; then recipient="$(age-keygen 2>/dev/null | awk '/public key:/ {print $4}')" printf 'AGE_RECIPIENT=%s\n' "$recipient" >>"${ETC}/.env" plain_before="$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')" run_backup "" >/dev/null expect "с AGE_RECIPIENT появился .age-файл" "1" "$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.age' | wc -l | tr -d ' ')" expect "новых незашифрованных архивов нет" "$plain_before" "$(find "${DATA}/backups" -maxdepth 1 -name 'glchat-*.tar.gz' | wc -l | tr -d ' ')" else printf 'AGE_RECIPIENT=age1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\n' >>"${ETC}/.env" if run_backup "" >/dev/null 2>&1; then fail "без установленного age бэкап не должен проходить молча" else ok "без age установщик бэкапа падает с ошибкой" fi fi mv "${ETC}/env.keep" "${ETC}/.env" # --- 6. rclone-приёмник: проверяем фактические команды rclone -------------- SHIM="${WORK}/bin" mkdir -p "$SHIM" cat >"${SHIM}/rclone" <<'SHIMEOF' #!/usr/bin/env bash printf 'rclone %s\n' "$*" >>"${RCLONE_LOG}" exit 0 SHIMEOF chmod +x "${SHIM}/rclone" rclone_log="${WORK}/rclone.log" : >"$rclone_log" ( export PATH="${SHIM}:${PATH}" RCLONE_LOG="$rclone_log" run_backup "rclone:remote:glchat" 7 >/dev/null ) if grep -q "^rclone copy .*glchat-.*\.tar\.gz remote:glchat$" "$rclone_log"; then ok "rclone copy вызван с целью remote:glchat" else fail "rclone copy не вызван: $(cat "$rclone_log")" fi if grep -q "^rclone delete --min-age 7d remote:glchat$" "$rclone_log"; then ok "rclone delete --min-age 7d вызван для ротации" else fail "offsite-ротация не вызвана: $(cat "$rclone_log")" fi printf '\n=== бэкап/offsite: %d проверок, %d ошибок ===\n' "$checks" "$errors" [ "$errors" -eq 0 ]