Files
felhom-controller/controller/internal/report/r1159_box_config_test.go
T

121 lines
4.2 KiB
Go

package report
import (
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"testing"
)
// R-1159 (controller v0.334.0): a live box refreshes its config with its OWN hub key; the retrieval passphrase is sent
// only to a hub that has no box-config route, or when the box holds no key yet — and never as a retry after the hub
// refused the key.
//
// COMPANION RED-PROOF (observed 2026-10-19): with PullConfigPreferKey reduced to the old PullConfig (the pre-fix
// refresh) the "new hub" case fails with
//
// r1159_box_config_test.go: new hub: "hub:\n api_key: box-key\n" <nil> (key pulls 0), want the config by the box key
//
// Restored.
func TestR1159_RefreshUsesTheBoxKey(t *testing.T) {
type seen struct {
mu sync.Mutex
passphrase, bearer int
}
hub := func(boxConfig int) (*httptest.Server, *seen) {
s := &seen{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
s.mu.Lock()
defer s.mu.Unlock()
switch {
case strings.HasPrefix(r.URL.Path, "/api/v1/box-config/"):
if boxConfig == 0 {
http.NotFound(w, r)
return
}
s.bearer++
if r.Header.Get("Authorization") != "Bearer box-key" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if boxConfig != 200 {
http.Error(w, "x", boxConfig)
return
}
w.Write([]byte("hub:\n api_key: box-key\n"))
case strings.HasPrefix(r.URL.Path, "/api/v1/config/"):
s.passphrase++
if r.Header.Get("X-Retrieval-Password") != "owner-pass" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
w.Write([]byte("hub:\n api_key: box-key\n"))
default:
http.NotFound(w, r)
}
}))
t.Cleanup(srv.Close)
return srv, s
}
logf := func(string, ...any) {}
srv, s := hub(200)
y, err := PullConfigPreferKey(srv.URL, "c1", "box-key", "owner-pass", logf)
if err != nil || !strings.Contains(y, "api_key") || s.bearer != 1 {
t.Fatalf("new hub: %q %v (key pulls %d), want the config by the box key", y, err, s.bearer)
}
if s.passphrase != 0 {
t.Fatalf("new hub: the passphrase was sent (%d time(s)) — a live box must refresh with its own key", s.passphrase)
}
srv, s = hub(0)
if y, err := PullConfigPreferKey(srv.URL, "c1", "box-key", "owner-pass", logf); err != nil || y == "" || s.passphrase != 1 {
t.Fatalf("old hub (no route): %v, passphrase pulls %d — want the fallback", err, s.passphrase)
}
srv, s = hub(200)
if _, err := PullConfigPreferKey(srv.URL, "c1", "", "owner-pass", logf); err != nil || s.passphrase != 1 || s.bearer != 0 {
t.Fatalf("no key yet: %v, passphrase %d bearer %d — want the passphrase pull", err, s.passphrase, s.bearer)
}
for _, code := range []int{http.StatusUnauthorized, http.StatusInternalServerError} {
srv, s = hub(code)
key := "box-key"
if code == http.StatusUnauthorized {
key = "wrong-key"
}
_, err := PullConfigPreferKey(srv.URL, "c1", key, "owner-pass", logf)
if err == nil || s.passphrase != 0 {
t.Fatalf("key refused (%d): err=%v passphrase pulls %d — the passphrase must not be the retry", code, err, s.passphrase)
}
if code == http.StatusUnauthorized && !errors.Is(err, ErrAuthFailed) {
t.Errorf("a refused key must read as ErrAuthFailed, got %v", err)
}
}
}
// The refresh the report ACK drives (cmd/controller main.go, ConfigRefresher.Refresh) must be the key-first pull — a
// PullConfigPreferKey nobody calls would leave every live box on the passphrase (the seam-never-wired shape).
func TestR1159_MainWiresTheKeyFirstRefresh(t *testing.T) {
b, err := os.ReadFile(filepath.Join("..", "..", "cmd", "controller", "main.go"))
if err != nil {
t.Fatalf("main.go: %v", err)
}
src := string(b)
i := strings.Index(src, "cr := &report.ConfigRefresher{")
if i < 0 {
t.Fatal("main.go no longer builds a report.ConfigRefresher — find the live refresh and pin it here")
}
block := src[i:]
if j := strings.Index(block, "cr.Reconcile("); j > 0 {
block = block[:j]
}
if !strings.Contains(block, "report.PullConfigPreferKey(") || !strings.Contains(block, "cfg.Hub.APIKey") {
t.Fatalf("the live config refresh does not use report.PullConfigPreferKey with the box's hub key:\n%s", block)
}
}