88b0a2e761
gates / gates (push) Successful in 30s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
88 lines
3.3 KiB
Go
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}
|
|
}
|