package api // v0.63.0 system-initiated immediacy — the api-side agent-plane pokes (admin desired-state write, // operator-peer bump) and the controller-plane intent bump on clean-slate re-enroll. The api sites // call the Poker SYNCHRONOUSLY, so a counting fake is deterministic right after the request. The // negatives (error/invalid paths, nil seams) carry explicit zero-count / no-panic assertions. import ( "net/http" "sync" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/intent" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // fakePoker counts calls per method and records PokeHost targets. Satisfies api.Poker. type fakePoker struct { mu sync.Mutex hostCalls []string allCalls int } func (f *fakePoker) PokeHost(hostID string) { f.mu.Lock() f.hostCalls = append(f.hostCalls, hostID) f.mu.Unlock() } func (f *fakePoker) PokeAllHosts() { f.mu.Lock() f.allCalls++ f.mu.Unlock() } func (f *fakePoker) hosts() []string { f.mu.Lock() defer f.mu.Unlock() return append([]string(nil), f.hostCalls...) } func (f *fakePoker) all() int { f.mu.Lock() defer f.mu.Unlock() return f.allCalls } // C1 — a successful admin desired-state write pokes exactly the target host (and never the fleet); // the invalid-JSON path pokes nothing (fire-after-commit). func TestAdminSetDesiredState_PokesTargetHostOnly(t *testing.T) { h, st, _ := newTestHandler(t) seedHost(t, st, "h1", "c1", "HKEY1") p := &fakePoker{} h.SetPoker(p) if rr := do(h, http.MethodPut, "/admin/hosts/h1/desired-state", globalKey, `{"guests":[]}`); rr.Code != http.StatusOK { t.Fatalf("admin-set = %d body=%s", rr.Code, rr.Body.String()) } if got := p.hosts(); len(got) != 1 || got[0] != "h1" { t.Fatalf("PokeHost targets = %v, want [h1]", got) } if p.all() != 0 { t.Errorf("PokeAllHosts = %d, want 0 (a single-host write is not a fleet nudge)", p.all()) } // Invalid JSON is rejected at the door → NO poke. before := len(p.hosts()) if rr := do(h, http.MethodPut, "/admin/hosts/h1/desired-state", globalKey, `not json`); rr.Code != http.StatusBadRequest { t.Fatalf("malformed admin-set = %d, want 400", rr.Code) } if len(p.hosts()) != before { t.Errorf("a poke fired on the invalid-JSON path (targets now %v)", p.hosts()) } } // C1 (nil seam) — an unset poker leaves the mutation intact and never panics. func TestAdminSetDesiredState_NilPokerNoPanic(t *testing.T) { h, st, _ := newTestHandler(t) seedHost(t, st, "h1", "c1", "HKEY1") // No SetPoker — h.poker is nil. if rr := do(h, http.MethodPut, "/admin/hosts/h1/desired-state", globalKey, `{"guests":[]}`); rr.Code != http.StatusOK { t.Fatalf("admin-set with nil poker = %d, want 200 (mutation must still succeed)", rr.Code) } } // C2 — a successful operator-peer write bumps EVERY host generation → exactly one fleet poke, and // never a single-host poke. (The BumpAllHostGenerations-error negative is assert-by-inspection: the // poke sits in the `else if h.poker != nil` arm of the bump result, so a bump error skips it — there // is no non-destructive seam to force a store bump failure in this fixture.) func TestAdminSetOperatorPeer_PokesFleet(t *testing.T) { h, st, _ := newTestHandler(t) seedHost(t, st, "h1", "c1", "HKEY1") putTestEndpoint(t, h) p := &fakePoker{} h.SetPoker(p) if rr := do(h, http.MethodPut, "/admin/wg/operator-peer", globalKey, `{"pubkey":"`+opTestPubkey+`","assigned_ip":"10.77.0.250"}`); rr.Code != http.StatusOK { t.Fatalf("set operator peer = %d body=%s", rr.Code, rr.Body.String()) } if p.all() != 1 { t.Errorf("PokeAllHosts = %d, want 1", p.all()) } if got := p.hosts(); len(got) != 0 { t.Errorf("PokeHost fired on a fleet-wide change: %v", got) } } // Scenario D — clean-slate re-enroll bumps the controller-plane intent so a long-polling controller // wakes in seconds. reissueOnReenroll is synchronous, so the generation advances by return time. // Red-proof: remove the Part-3 bump → Generation stays 0. func TestReenroll_BumpsIntent(t *testing.T) { h, st, _ := newTestHandler(t) st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "pass-phrase"}) hub := intent.New() h.SetIntentHub(hub) gen0 := hub.Generation("c1") if rr := doEnroll(h, "c1", "pass-phrase"); rr.Code != http.StatusCreated { t.Fatalf("enroll = %d body=%s", rr.Code, rr.Body.String()) } if gen := hub.Generation("c1"); gen <= gen0 { t.Fatalf("intent generation = %d, want > %d (re-enroll must bump)", gen, gen0) } } // Scenario D (nil seam) — with no intent hub wired, enroll still succeeds and never panics. func TestReenroll_NilIntentHubNoPanic(t *testing.T) { h, st, _ := newTestHandler(t) st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "pass-phrase"}) // No SetIntentHub — h.intentHub is nil. if rr := doEnroll(h, "c1", "pass-phrase"); rr.Code != http.StatusCreated { t.Fatalf("enroll with nil intent hub = %d, want 201", rr.Code) } }