Files
felhom.eu/hub/internal/osupdates/service_test.go
T

201 lines
6.4 KiB
Go

package osupdates
import (
"log"
"os"
"path/filepath"
"strings"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
type fix struct {
s *Service
now time.Time
events []string
bumps []string
}
func newFix(t *testing.T) *fix {
t.Helper()
st, err := store.New(filepath.Join(t.TempDir(), "hub.db"), log.New(os.Stderr, "", 0))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { st.Close() })
for _, h := range []struct{ host, cust string }{{"hp", "c-hp"}, {"n100", "c-n100"}, {"cust1", "c-1"}} {
if err := st.UpsertHost(&store.Host{HostID: h.host, CustomerID: h.cust, APIKey: "k-" + h.host}); err != nil {
t.Fatal(err)
}
}
f := &fix{now: time.Date(2026, 10, 4, 12, 0, 0, 0, time.UTC)}
f.s = &Service{Store: st, ApproveAfter: 24 * time.Hour, NightsRequired: 1, Now: func() time.Time { return f.now },
Emit: func(_, typ, _, _, _, _ string) { f.events = append(f.events, typ) },
Bump: func(h string) { f.bumps = append(f.bumps, h) }}
_ = st.SetOSRing("hp", 0)
_ = st.SetOSRing("n100", 0)
return f
}
func pk(name, ver string) Package { return Package{Name: name, Version: ver, Origin: "Debian"} }
func (f *fix) report(t *testing.T, host, trigger string, healthy bool, pkgs ...Package) {
t.Helper()
f.reportL(t, host, LayerGuest, trigger, healthy, pkgs...)
}
func (f *fix) reportL(t *testing.T, host, layer, trigger string, healthy bool, pkgs ...Package) {
t.Helper()
outcome := "applied"
if !healthy {
outcome = "health_failed"
}
if err := f.s.Ingest(host, Report{RunID: host + trigger + f.now.String(), Layer: layer, Trigger: trigger, Mode: "apply", Outcome: outcome,
Healthy: healthy, Installed: pkgs, Upgraded: pkgs[:1]}); err != nil {
t.Fatal(err)
}
}
// The ruled wait: approved only after every ring-0 box ran the set healthy for ApproveAfter AND through a night run.
// Red-proof: drop the age check, the healthy check or the nights check in Evaluate and a sub-step fails.
func TestApproval_WaitHealthyAndOneNight(t *testing.T) {
f := newFix(t)
set := []Package{pk("libc6", "2.41-12+deb13u4"), pk("openssl", "3.5.7-1~deb13u3")}
f.report(t, "hp", "debug", true, set...)
f.report(t, "n100", "debug", true, set...)
sts, _ := f.s.Evaluate()
st := sts[0]
if !strings.HasPrefix(st.Waiting, "healthy for") {
t.Fatalf("fresh set approved or wrong reason: %+v", st)
}
f.now = f.now.Add(25 * time.Hour)
sts, _ = f.s.Evaluate()
st = sts[0]
if !strings.Contains(st.Waiting, "night run") {
t.Fatalf("approved without a night run: %+v", st)
}
f.report(t, "hp", "night", true, set...)
f.report(t, "n100", "night", true, set...)
if _, err := f.s.Evaluate(); err != nil {
t.Fatal(err)
}
rel, _ := f.s.Store.LatestOSRelease(LayerGuest)
if rel == nil || rel.ApprovedBy != "auto" {
t.Fatalf("not approved: %+v", rel)
}
if len(f.bumps) != 1 || f.bumps[0] != "cust1" {
t.Fatalf("only the ring-1 box must be nudged, got %v", f.bumps)
}
b := f.s.DesiredBlock("cust1")
if b.Ring != 1 || !b.Enabled || b.Release == nil || len(b.Release.Packages) != 2 || b.Release.Snapshot != rel.ApprovedAt.UTC().Format("20060102T150405Z") {
t.Fatalf("ring-1 block = %+v", b)
}
if b.HostRelease != nil {
t.Fatalf("a guest approval must not create a host release (separate sets): %+v", b.HostRelease)
}
if hb := f.s.DesiredBlock("hp"); hb.Ring != 0 || hb.Release != nil {
t.Fatalf("a ring-0 box must not get a release (it installs everything pending): %+v", hb)
}
}
func TestApproval_UnhealthyRunBlocks(t *testing.T) {
f := newFix(t)
set := []Package{pk("libc6", "2.41-12+deb13u4")}
f.report(t, "hp", "debug", true, set...)
f.report(t, "n100", "debug", true, set...)
f.s.Evaluate()
f.now = f.now.Add(25 * time.Hour)
f.report(t, "hp", "night", false, set...)
f.report(t, "n100", "night", true, set...)
sts, _ := f.s.Evaluate()
st := sts[0]
if rel, _ := f.s.Store.LatestOSRelease(LayerGuest); rel != nil {
t.Fatalf("approved although a ring-0 run was not healthy: %+v", st)
}
if !strings.Contains(st.Waiting, "healthy=false") {
t.Fatalf("reason = %q", st.Waiting)
}
found := false
for _, e := range f.events {
found = found || e == EventHealthFailed
}
if !found {
t.Fatalf("no %s event: %v", EventHealthFailed, f.events)
}
}
// Ring 0 disagreeing on a package: that package is left out of the candidate (C9 — approve what ALL ring 0 runs).
func TestCandidate_DisagreementLeftOut(t *testing.T) {
f := newFix(t)
f.report(t, "hp", "night", true, pk("libc6", "2.41-12+deb13u4"), pk("curl", "8.14.1-2+deb13u5"))
f.report(t, "n100", "night", true, pk("libc6", "2.41-12+deb13u4"), pk("curl", "8.14.1-2+deb13u4"))
cand, err := f.s.candidate(LayerGuest, []string{"hp", "n100"})
if err != nil {
t.Fatal(err)
}
if _, ok := cand["curl"]; ok || cand["libc6"].Version != "2.41-12+deb13u4" {
t.Fatalf("candidate = %+v", cand)
}
}
// Non-Debian origins never enter a release (the fast lane is Debian / Debian-Security only, C3).
func TestCandidate_OnlyDebianOrigins(t *testing.T) {
f := newFix(t)
d := Package{Name: "docker-ce", Version: "5:29.8.2", Origin: "Docker"}
f.report(t, "hp", "night", true, pk("libc6", "x1"), d)
f.report(t, "n100", "night", true, pk("libc6", "x1"), d)
cand, _ := f.s.candidate(LayerGuest, []string{"hp", "n100"})
if _, ok := cand["docker-ce"]; ok {
t.Fatal("a Docker package entered the candidate")
}
}
func TestApproveNow_IsAnOperatorEvent(t *testing.T) {
f := newFix(t)
f.report(t, "hp", "debug", true, pk("libc6", "x1"))
f.report(t, "n100", "debug", true, pk("libc6", "x1"))
id, err := f.s.ApproveNow()
if err != nil || id == "" {
t.Fatalf("%q %v", id, err)
}
rel, _ := f.s.Store.LatestOSRelease(LayerGuest)
if rel.ApprovedBy != "operator" {
t.Fatalf("approved_by = %q", rel.ApprovedBy)
}
n := 0
for _, e := range f.events {
if e == EventApprovedNow {
n++
}
}
if n != 1 {
t.Fatalf("want one %s, got %v", EventApprovedNow, f.events)
}
}
func TestSwitchAndRing(t *testing.T) {
f := newFix(t)
if err := f.s.SetEnabled("cust1", false); err != nil {
t.Fatal(err)
}
if b := f.s.DesiredBlock("cust1"); b.Enabled {
t.Fatal("switch OFF not delivered")
}
if err := f.s.SetRing("cust1", 2); err == nil {
t.Fatal("ring 2 accepted")
}
if b := f.s.DesiredBlock("nobody-row"); b.Ring != 1 || !b.Enabled {
t.Fatalf("default must be ring 1, ON: %+v", b)
}
}
func TestIngest_AppliedIsTheHouseholdsLine(t *testing.T) {
f := newFix(t)
f.report(t, "cust1", "night", true, pk("libc6", "x1"))
if len(f.events) != 1 || f.events[0] != EventApplied {
t.Fatalf("events = %v", f.events)
}
}