121 lines
4.2 KiB
Go
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)
|
|
}
|
|
}
|