Post-repair hardware validation pipeline for Proxmox cluster hosts. Go orchestrator + in-image agent + mkosi live image + bundled dnsmasq PXE + SQLite + HTMX/SSE UI + notify registry + janitor + full docs.
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
package logs_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"vetting/internal/events"
|
||||
"vetting/internal/logs"
|
||||
)
|
||||
|
||||
// TestAppendFansOutToSSE verifies the two guarantees of the log hub:
|
||||
// (a) every line is persisted to the per-run file, and (b) every line
|
||||
// is published as an SSE event with name log-<runID>. The UI relies on
|
||||
// both — the file for reload replay, the event for live tail.
|
||||
func TestAppendFansOutToSSE(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
hub := events.NewHub()
|
||||
lh, err := logs.NewHub(dir, hub)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHub: %v", err)
|
||||
}
|
||||
defer lh.Close()
|
||||
|
||||
_, ch, cancel := hub.Subscribe()
|
||||
defer cancel()
|
||||
|
||||
w, err := lh.WriterFor(77)
|
||||
if err != nil {
|
||||
t.Fatalf("WriterFor: %v", err)
|
||||
}
|
||||
w.Append(logs.Line{Level: "info", Text: "hello from agent"})
|
||||
w.Append(logs.Line{Level: "error", Text: "<script>pwn</script>"})
|
||||
|
||||
got := collect(ch, 3, 500*time.Millisecond)
|
||||
// Filter out heartbeats that may sneak in.
|
||||
var logEvents []events.Event
|
||||
for _, ev := range got {
|
||||
if strings.HasPrefix(ev.Name, "log-") {
|
||||
logEvents = append(logEvents, ev)
|
||||
}
|
||||
}
|
||||
if len(logEvents) < 2 {
|
||||
t.Fatalf("expected 2 log events, got %d (all=%+v)", len(logEvents), got)
|
||||
}
|
||||
for _, ev := range logEvents {
|
||||
if ev.Name != "log-77" {
|
||||
t.Fatalf("unexpected event name %q", ev.Name)
|
||||
}
|
||||
}
|
||||
// XSS protection: raw <script> must not appear — it's HTML-escaped.
|
||||
if strings.Contains(logEvents[1].Payload, "<script>") {
|
||||
t.Fatalf("log payload not escaped: %q", logEvents[1].Payload)
|
||||
}
|
||||
if !strings.Contains(logEvents[1].Payload, "<script>") {
|
||||
t.Fatalf("expected escaped <script>, got %q", logEvents[1].Payload)
|
||||
}
|
||||
|
||||
// On disk: the file must contain both lines.
|
||||
path := filepath.Join(dir, "run-77.log")
|
||||
body, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read log file: %v", err)
|
||||
}
|
||||
text := string(body)
|
||||
if !strings.Contains(text, "hello from agent") {
|
||||
t.Fatalf("disk log missing info line: %q", text)
|
||||
}
|
||||
if !strings.Contains(text, "<script>pwn</script>") {
|
||||
t.Fatalf("disk log should keep raw text (unescaped): %q", text)
|
||||
}
|
||||
if !strings.Contains(text, "INFO") || !strings.Contains(text, "ERROR") {
|
||||
t.Fatalf("disk log missing level prefix: %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWriterForIsCached verifies a second call returns the same Writer
|
||||
// — otherwise parallel /log POSTs would race on file opens and possibly
|
||||
// stomp on in-flight writes.
|
||||
func TestWriterForIsCached(t *testing.T) {
|
||||
hub := events.NewHub()
|
||||
lh, err := logs.NewHub(t.TempDir(), hub)
|
||||
if err != nil {
|
||||
t.Fatalf("NewHub: %v", err)
|
||||
}
|
||||
defer lh.Close()
|
||||
|
||||
w1, err := lh.WriterFor(1)
|
||||
if err != nil {
|
||||
t.Fatalf("WriterFor: %v", err)
|
||||
}
|
||||
w2, err := lh.WriterFor(1)
|
||||
if err != nil {
|
||||
t.Fatalf("WriterFor: %v", err)
|
||||
}
|
||||
if w1 != w2 {
|
||||
t.Fatalf("Writer not cached: %p vs %p", w1, w2)
|
||||
}
|
||||
}
|
||||
|
||||
// collect drains up to max events or bails after deadline.
|
||||
func collect(ch <-chan events.Event, max int, deadline time.Duration) []events.Event {
|
||||
out := []events.Event{}
|
||||
timer := time.NewTimer(deadline)
|
||||
defer timer.Stop()
|
||||
for len(out) < max {
|
||||
select {
|
||||
case ev, ok := <-ch:
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
out = append(out, ev)
|
||||
case <-timer.C:
|
||||
return out
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user