health_test.go
⎇
Raw
1package server
2
3import (
4 "encoding/json"
5 "net/http"
6 "net/http/httptest"
7 "os"
8 "path/filepath"
9 "testing"
10 "time"
11)
12
13// fakeTool writes an executable stand-in for one of the external tools and
14// points the matching environment variable at it. Must be called before
15// setupTestServer, which is what reads the configuration.
16func fakeTool(t *testing.T, envVar, name, body string) {
17 t.Helper()
18 path := filepath.Join(t.TempDir(), name)
19 if err := os.WriteFile(path, []byte("#!/bin/sh\n"+body+"\n"), 0o755); err != nil {
20 t.Fatal(err)
21 }
22 t.Setenv(envVar, path)
23}
24
25func getHealth(t *testing.T, router http.Handler) (*httptest.ResponseRecorder, map[string]any) {
26 t.Helper()
27 w := getWith(router, "/healthz", nil)
28 var body map[string]any
29 if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
30 t.Fatalf("decode /healthz body %q: %v", w.Body.String(), err)
31 }
32 return w, body
33}
34
35func dependencies(t *testing.T, body map[string]any) map[string]any {
36 t.Helper()
37 deps, ok := body["dependencies"].(map[string]any)
38 if !ok {
39 t.Fatalf("no dependencies object in %v", body)
40 }
41 return deps
42}
43
44func TestHealthReportsToolVersions(t *testing.T) {
45 fakeTool(t, "VIDARCHIVE_YTDLP_PATH", "yt-dlp", `echo 2026.01.01`)
46 fakeTool(t, "VIDARCHIVE_FFMPEG_PATH", "ffmpeg", `echo "ffmpeg version 7.1 Copyright (c) 2000-2024"`)
47 fakeTool(t, "VIDARCHIVE_FFPROBE_PATH", "ffprobe", `echo "ffprobe version 7.1 Copyright (c) 2000-2024"`)
48
49 srv, _, cleanup := setupTestServer(t)
50 defer cleanup()
51
52 w, body := getHealth(t, srv.Router())
53 if w.Code != http.StatusOK {
54 t.Errorf("status = %d, want 200", w.Code)
55 }
56 if got := body["status"]; got != "ok" {
57 t.Errorf("status field = %v, want ok", got)
58 }
59
60 deps := dependencies(t, body)
61 if got := deps["database"]; got != "ok" {
62 t.Errorf("database = %v, want ok", got)
63 }
64 if got := deps["yt-dlp"]; got != "2026.01.01" {
65 t.Errorf("yt-dlp = %v, want 2026.01.01", got)
66 }
67 // "ffmpeg version 7.1 ..." is reduced to the version token.
68 if got := deps["ffmpeg"]; got != "7.1" {
69 t.Errorf("ffmpeg = %v, want 7.1", got)
70 }
71}
72
73// A missing external tool is reported but does not fail the check: the UI still
74// works, only downloads are affected.
75func TestHealthMissingToolIsNotFatal(t *testing.T) {
76 t.Setenv("VIDARCHIVE_YTDLP_PATH", "/nonexistent/yt-dlp")
77
78 srv, _, cleanup := setupTestServer(t)
79 defer cleanup()
80
81 w, body := getHealth(t, srv.Router())
82 if w.Code != http.StatusOK {
83 t.Errorf("status = %d, want 200 for a missing tool", w.Code)
84 }
85 if got := body["status"]; got != "ok" {
86 t.Errorf("status field = %v, want ok", got)
87 }
88 if got := dependencies(t, body)["yt-dlp"]; got != "not installed" {
89 t.Errorf("yt-dlp = %v, want \"not installed\"", got)
90 }
91}
92
93// An unreachable database is the one condition that must fail the check, so an
94// orchestrator can act on it.
95func TestHealthUnreachableDatabaseIs503(t *testing.T) {
96 srv, _, db, cleanup := setupTestServerDB(t)
97 defer cleanup()
98
99 if err := db.Close(); err != nil {
100 t.Fatalf("close db: %v", err)
101 }
102
103 w, body := getHealth(t, srv.Router())
104 if w.Code != http.StatusServiceUnavailable {
105 t.Errorf("status = %d, want 503", w.Code)
106 }
107 if got := body["status"]; got != "unhealthy" {
108 t.Errorf("status field = %v, want unhealthy", got)
109 }
110 if got := dependencies(t, body)["database"]; got != "unreachable" {
111 t.Errorf("database = %v, want unreachable", got)
112 }
113}
114
115// A wedged tool must not wedge the probe: an orchestrator reads a stuck request
116// as "still starting" and never restarts the container.
117func TestHealthBoundsAHangingTool(t *testing.T) {
118 fakeTool(t, "VIDARCHIVE_YTDLP_PATH", "yt-dlp", `sleep 60`)
119
120 srv, _, cleanup := setupTestServer(t)
121 defer cleanup()
122
123 start := time.Now()
124 w, body := getHealth(t, srv.Router())
125 elapsed := time.Since(start)
126
127 // The handler allows 3s; anything near the 60s sleep means it is unbounded.
128 if elapsed > 15*time.Second {
129 t.Errorf("probe took %v, want it bounded by the handler timeout", elapsed)
130 }
131 if w.Code != http.StatusOK {
132 t.Errorf("status = %d, want 200", w.Code)
133 }
134 if got := dependencies(t, body)["yt-dlp"]; got != "timed out" {
135 t.Errorf("yt-dlp = %v, want \"timed out\"", got)
136 }
137}
138