Files
felhom.eu/hub/internal/api/os_updates_test.go
T

88 lines
3.3 KiB
Go

package api
import (
"encoding/json"
"log"
"net/http"
"os"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-hub/internal/osupdates"
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
)
// The os_update block a box receives is a contract DUPLICATED with felhom-agent:
// testdata/desired-state-osupdate.golden.json MUST stay byte-identical with the agent's
// internal/hub/testdata copy (the agent's test decodes it). This test proves the hub SERVES exactly that shape.
func TestOSUpdate_DesiredBlockMatchesTheGolden(t *testing.T) {
h, st, _ := newTestHandler(t)
seedHost(t, st, "h1", "c1", "HKEY1")
svc := &osupdates.Service{Store: st, ApproveAfter: 0, NightsRequired: 0, Logger: log.New(os.Stderr, "", 0)}
h.SetOSUpdateService(svc)
rel := `[{"name":"libc6","version":"2.41-12+deb13u4","origin":"Debian"},{"name":"openssl","version":"3.5.7-1~deb13u3","origin":"Debian-Security"}]`
if err := st.SaveOSRelease(storeRelease("os-guest-20261004-120000", "guest", rel)); err != nil {
t.Fatal(err)
}
hrel := `[{"name":"libssl3t64","version":"3.5.7-1~deb13u3","origin":"Debian-Security"}]`
if err := st.SaveOSRelease(storeRelease("os-host-20261004-120000", "host", hrel)); err != nil {
t.Fatal(err)
}
rr := do(h, http.MethodGet, "/hosts/h1/desired-state", "HKEY1", "")
if rr.Code != 200 {
t.Fatalf("GET desired-state: %d", rr.Code)
}
var got struct {
DesiredState struct {
OSUpdate json.RawMessage `json:"os_update"`
} `json:"desired_state"`
}
json.Unmarshal(rr.Body.Bytes(), &got)
raw, err := os.ReadFile("testdata/desired-state-osupdate.golden.json")
if err != nil {
t.Fatal(err)
}
var golden struct {
DesiredState struct {
OSUpdate json.RawMessage `json:"os_update"`
} `json:"desired_state"`
}
if err := json.Unmarshal(raw, &golden); err != nil {
t.Fatal(err)
}
var a, b any
json.Unmarshal(got.DesiredState.OSUpdate, &a)
json.Unmarshal(golden.DesiredState.OSUpdate, &b)
ab, _ := json.Marshal(a)
bb, _ := json.Marshal(b)
if string(ab) != string(bb) {
t.Fatalf("served os_update diverged from the golden:\n served: %s\n golden: %s", ab, bb)
}
}
// A box reports only for itself; another box's key is refused and nothing is stored.
func TestOSReport_SelfScoped(t *testing.T) {
h, st, _ := newTestHandler(t)
seedHost(t, st, "h1", "c1", "HKEY1")
seedHost(t, st, "h2", "c2", "HKEY2")
h.SetOSUpdateService(&osupdates.Service{Store: st})
body := `{"run_id":"r1","trigger":"night","mode":"apply","outcome":"applied","healthy":true,"upgraded":[{"name":"libc6","version":"x","origin":"Debian"}]}`
if rr := do(h, http.MethodPost, "/hosts/h1/os-report", "HKEY2", body); rr.Code != http.StatusForbidden {
t.Fatalf("cross-host report → %d, want 403", rr.Code)
}
if r, _ := st.LatestOSReport("h1", "guest"); r != nil {
t.Fatal("a refused report was stored")
}
if rr := do(h, http.MethodPost, "/hosts/h1/os-report", "HKEY1", body); rr.Code != http.StatusOK {
t.Fatalf("own report → %d %s", rr.Code, rr.Body.String())
}
if r, _ := st.LatestOSReport("h1", "guest"); r == nil || r.Outcome != "applied" {
t.Fatalf("report not stored: %+v", r)
}
}
func storeRelease(id, layer, pkgs string) store.OSRelease {
at, _ := time.Parse(time.RFC3339, "2026-10-04T12:00:00Z")
return store.OSRelease{ID: id, Layer: layer, Fingerprint: "fp-" + layer, ApprovedAt: at, ApprovedBy: "auto", PackagesJSON: pkgs}
}