Files

199 lines
6.1 KiB
Go
Raw Permalink Normal View History

2026-05-22 20:26:11 -04:00
package handlers
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/Silo-Server/silo-server/internal/buildinfo"
"github.com/Silo-Server/silo-server/internal/nodepool"
"github.com/Silo-Server/silo-server/internal/playback"
2026-05-22 20:26:11 -04:00
)
func TestSystemBuildInfoResponse(t *testing.T) {
t.Parallel()
handler := &SystemHandler{
buildInfo: buildinfo.Info{
Display: "b4c5aae1+dirty",
Revision: "b4c5aae18aa653725ac697b29a05eac797576008",
Dirty: true,
VCSTime: "2026-04-05T22:24:40Z",
Available: true,
},
}
router := chi.NewRouter()
router.Get("/admin/system/build", handler.HandleBuildInfo)
req := httptest.NewRequest(http.MethodGet, "/admin/system/build", nil)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var got buildinfo.Info
if err := json.NewDecoder(rec.Body).Decode(&got); err != nil {
t.Fatalf("decoding response: %v", err)
}
want := handler.buildInfo
if got != want {
t.Fatalf("response = %#v, want %#v", got, want)
}
}
func TestSystemBuildInfoUnavailableResponseShape(t *testing.T) {
t.Parallel()
handler := &SystemHandler{
buildInfo: buildinfo.Info{
Display: "unavailable",
Revision: "",
Dirty: false,
VCSTime: "",
Available: false,
},
}
req := httptest.NewRequest(http.MethodGet, "/admin/system/build", nil)
rec := httptest.NewRecorder()
handler.HandleBuildInfo(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var raw map[string]any
if err := json.Unmarshal(rec.Body.Bytes(), &raw); err != nil {
t.Fatalf("decoding raw response: %v", err)
}
expected := map[string]any{
"display": "unavailable",
"revision": "",
"dirty": false,
"vcs_time": "",
"available": false,
}
for key, want := range expected {
if got, ok := raw[key]; !ok || got != want {
t.Fatalf("response[%q] = %#v (present=%v), want %#v", key, got, ok, want)
}
}
}
func TestHandleHWAccelAggregatesAllHealthyNodes(t *testing.T) {
t.Parallel()
nodeA := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/hw-capabilities" {
http.NotFound(w, r)
return
}
json.NewEncoder(w).Encode(playback.HWAccelInfo{
Resolved: "qsv",
RenderDevices: []string{"/dev/dri/renderD128", "/dev/dri/renderD129"},
RenderDeviceDetails: []playback.RenderDeviceInfo{
{Path: "/dev/dri/renderD128", Description: "Intel GPU"},
{Path: "/dev/dri/renderD129", Description: "Intel GPU"},
},
})
}))
defer nodeA.Close()
nodeB := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(playback.HWAccelInfo{
Resolved: "vaapi",
RenderDevices: []string{"/dev/dri/renderD128"},
RenderDeviceDetails: []playback.RenderDeviceInfo{
{Path: "/dev/dri/renderD128", Description: "AMD GPU"},
},
})
}))
defer nodeB.Close()
nodeDown := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "boom", http.StatusInternalServerError)
}))
defer nodeDown.Close()
pool := nodepool.NewTranscodePool()
pool.SetNodes([]*nodepool.Node{
{ID: 1, Name: "node-a", URL: nodeA.URL, Enabled: true, Healthy: true},
{ID: 2, Name: "node-b", URL: nodeB.URL, Enabled: true, Healthy: true},
{ID: 3, Name: "node-down", URL: nodeDown.URL, Enabled: true, Healthy: true},
{ID: 4, Name: "node-unhealthy", URL: "http://unreachable.invalid", Enabled: true, Healthy: false},
})
handler := &SystemHandler{transcodePool: pool, jwtSecret: "secret"}
req := httptest.NewRequest(http.MethodGet, "/admin/system/hw-accel", nil)
rec := httptest.NewRecorder()
handler.HandleHWAccel(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var got HWAccelInventory
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("decoding response: %v", err)
}
// Flat primary fields keep the historical single-probe shape, sourced
// from the first healthy node that answered.
if got.Resolved != "qsv" || got.Source != "transcode_node" || got.NodeURL != nodeA.URL {
t.Fatalf("primary = resolved %q source %q node %q, want first node's probe", got.Resolved, got.Source, got.NodeURL)
}
// Every healthy node appears, in pool order; the unhealthy one does not.
if len(got.Nodes) != 3 {
t.Fatalf("nodes len = %d, want 3: %+v", len(got.Nodes), got.Nodes)
}
if got.Nodes[0].NodeName != "node-a" || got.Nodes[0].Resolved != "qsv" || len(got.Nodes[0].RenderDeviceDetails) != 2 {
t.Fatalf("node-a entry = %+v", got.Nodes[0])
}
if got.Nodes[1].NodeName != "node-b" || got.Nodes[1].Resolved != "vaapi" || len(got.Nodes[1].RenderDevices) != 1 {
t.Fatalf("node-b entry = %+v", got.Nodes[1])
}
if got.Nodes[2].NodeName != "node-down" || got.Nodes[2].Error == "" {
t.Fatalf("failed node entry = %+v, want populated error", got.Nodes[2])
}
}
func TestHandleHWAccelAllNodeProbesFailFallsBackLocal(t *testing.T) {
t.Parallel()
nodeDown := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "boom", http.StatusInternalServerError)
}))
defer nodeDown.Close()
pool := nodepool.NewTranscodePool()
pool.SetNodes([]*nodepool.Node{
{ID: 1, Name: "node-down", URL: nodeDown.URL, Enabled: true, Healthy: true},
})
handler := &SystemHandler{transcodePool: pool, jwtSecret: "secret"}
req := httptest.NewRequest(http.MethodGet, "/admin/system/hw-accel", nil)
rec := httptest.NewRecorder()
handler.HandleHWAccel(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var got HWAccelInventory
if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
t.Fatalf("decoding response: %v", err)
}
if got.Source != "local" {
t.Fatalf("source = %q, want local fallback when every node probe fails", got.Source)
}
if len(got.Nodes) != 1 || got.Nodes[0].Error == "" {
t.Fatalf("nodes = %+v, want the failing node listed with its error", got.Nodes)
}
}