// hooks_test.go — pre/post backup hook semantics (P2R-11). package runner import ( "context" "strings" "testing" "gitea.dcglab.co.uk/steve/restic-manager/internal/api" ) // TestPreHookFailureAbortsBackup: pre_hook exits 1 → restic never // runs, job is recorded failed with the hook's error. func TestPreHookFailureAbortsBackup(t *testing.T) { t.Parallel() // Restic script that records every invocation. If restic was // called we'll see "restic-was-here" in the captured log. bin := setupScript(t, `echo "restic-was-here"`) tx := &fakeSender{} r := New(Config{ResticBin: bin}, tx, 0) err := r.RunBackup(context.Background(), "job-pre", []string{"/etc"}, nil, []string{"tag"}, BackupHooks{Pre: "exit 1"}) if err == nil { t.Fatal("expected RunBackup to return an error from failed pre_hook") } if !strings.Contains(err.Error(), "pre_hook failed") { t.Fatalf("error message: %q (want 'pre_hook failed')", err) } // job.finished arrived with status=failed. finEnv := firstEnvOfType(t, tx.envs, api.MsgJobFinished) var fin api.JobFinishedPayload _ = finEnv.UnmarshalPayload(&fin) if fin.Status != api.JobFailed { t.Fatalf("status: %q, want failed", fin.Status) } // restic must NOT have run. for _, env := range tx.envs { if env.Type != api.MsgLogStream { continue } var l api.LogStreamLine _ = env.UnmarshalPayload(&l) if strings.Contains(l.Payload, "restic-was-here") { t.Fatal("restic was invoked despite pre_hook failure") } } } // TestPostHookRunsAfterBackup: post_hook fires after a successful // backup and receives RM_JOB_STATUS=succeeded in the env. func TestPostHookRunsAfterBackup(t *testing.T) { t.Parallel() bin := setupScript(t, ` case "$1" in backup) echo '{"message_type":"summary","snapshot_id":"abc"}' ;; snapshots) echo '[]' ;; stats) echo '{"total_size":0,"total_uncompressed_size":0,"snapshots_count":0,"total_file_count":0,"total_blob_count":0}' ;; *) exit 0 ;; esac `) tx := &fakeSender{} r := New(Config{ResticBin: bin}, tx, 0) post := `echo "post-status=$RM_JOB_STATUS phase=$RM_HOOK_PHASE"` if err := r.RunBackup(context.Background(), "job-post", []string{"/etc"}, nil, nil, BackupHooks{Post: post}); err != nil { t.Fatalf("RunBackup: %v", err) } // Walk log.stream envelopes; one of them should be the post-hook // line with the expected status. var found bool for _, env := range tx.envs { if env.Type != api.MsgLogStream { continue } var l api.LogStreamLine _ = env.UnmarshalPayload(&l) if strings.Contains(l.Payload, "post-status=succeeded") && strings.Contains(l.Payload, "phase=post") { found = true break } } if !found { t.Fatal("post_hook output not found in log.stream envelopes") } }