package osupdate import ( "context" "encoding/json" "testing" "time" "gitea.dooplex.hu/admin/felhom-agent/internal/hub" ) // ---- the security-profile refresh (R-1388, R-1301, `11` §5.12) ---- func profileCalls(w *fakeWrapper) []map[string]any { var m []map[string]any for _, p := range w.plans { if p["mode"] == "profile-refresh" { m = append(m, p) } } return m } func refreshed() *WrapperReport { return &WrapperReport{ProfileRefresh: json.RawMessage(`{"outcome":"refreshed","kernel":"7.0.14-28-pve","rewritten":["f6da0254.0/usr.sbin.chronyd"],` + `"restarted":[{"unit":"chrony.service","program":"/usr/sbin/chronyd","rc":0,"active":true}],"seconds":0.5}`)} } // One call, a host-layer plan that names NOTHING, and nothing to the hub. // COMPANION RED-PROOF (observed): add "units" to the plan → this test fails; drop the Appliance check → the BYO test fails. func TestProfileRefresh_OneCallNoNamesNoHubReport(t *testing.T) { w := &fakeWrapper{t: t, profileRep: refreshed()} l, h := newLeg(t, w, &hub.WireOSUpdate{Ring: 1, Enabled: true}) if got := l.ProfileRefresh(context.Background(), "agent start", nil, 1, time.Second); got != "refreshed" { t.Fatalf("outcome = %q", got) } pc := profileCalls(w) if len(pc) != 1 { t.Fatalf("calls = %d", len(pc)) } if len(pc[0]) != 3 || pc[0]["layer"] != LayerHost || pc[0]["release_id"] != "profile-refresh" { t.Fatalf("the plan carries more than release_id, layer and mode: %v", pc[0]) } if len(h.reports) != 0 { t.Fatalf("the hub got a report: %+v", h.reports) } } func TestProfileRefresh_ByoHostIsNeverAsked(t *testing.T) { w := &fakeWrapper{t: t, profileRep: refreshed()} l, _ := newLeg(t, w, nil) l.Appliance = false if got := l.ProfileRefresh(context.Background(), "agent start", nil, 1, time.Second); got != "" || len(profileCalls(w)) != 0 { t.Fatalf("a BYO host was asked: %q %v", got, profileCalls(w)) } } // The heavy-op gate: busy → wait and ask again; still busy after the tries → not run (the next start asks again). // COMPANION RED-PROOF (observed): ignore `ok` from the gate → the "always busy" half fails. func TestProfileRefresh_WaitsForTheHeavyOpGate(t *testing.T) { w := &fakeWrapper{t: t, profileRep: refreshed()} l, _ := newLeg(t, w, nil) n, released := 0, 0 gate := func() (func(), string, bool) { n++ if n < 3 { return nil, "primary-backup", false } return func() { released++ }, "", true } if got := l.ProfileRefresh(context.Background(), "agent start", gate, 5, time.Minute); got != "refreshed" { t.Fatalf("outcome = %q after %d gate tries", got, n) } if n != 3 || released != 1 || len(profileCalls(w)) != 1 { t.Fatalf("gate tries %d, released %d, calls %d", n, released, len(profileCalls(w))) } w2 := &fakeWrapper{t: t, profileRep: refreshed()} l2, _ := newLeg(t, w2, nil) busy := func() (func(), string, bool) { return nil, "restore-test", false } if got := l2.ProfileRefresh(context.Background(), "agent start", busy, 3, time.Minute); got != "" || len(profileCalls(w2)) != 0 { t.Fatalf("ran beside a heavy operation: %q %d call(s)", got, len(profileCalls(w2))) } } // An older wrapper refuses the mode: logged, nothing else. A failure is said as "failed". func TestProfileRefresh_RefusedAndFailed(t *testing.T) { w := &fakeWrapper{t: t, profileRep: &WrapperReport{Refused: json.RawMessage(`{"code":"R11","reason":"unknown mode 'profile-refresh'"}`)}} l, _ := newLeg(t, w, nil) if got := l.ProfileRefresh(context.Background(), "agent start", nil, 1, time.Second); got != "" { t.Fatalf("refused → %q", got) } w2 := &fakeWrapper{t: t, profileRep: &WrapperReport{Failed: json.RawMessage(`{"rc":1,"step":"profile-refresh"}`)}} l2, _ := newLeg(t, w2, nil) if got := l2.ProfileRefresh(context.Background(), "agent start", nil, 1, time.Second); got != "failed" { t.Fatalf("failed → %q", got) } }