package bootstrap import ( "context" "errors" "io" "net" "net/http" "strings" "sync/atomic" "testing" "time" "gitea.dooplex.hu/admin/felhom-controller/internal/config" ) // R-554 (`09` §3 decision 222): the setup wizard is deleted. A box whose first ingestion FAILED serves the // waiting page — in both languages, on every path, with a passing health answer — and not a 404 or a // wizard; it retries, and once the hub answers it returns the configured config and closes the page. // // RED-PROOF (REPORT): make WaitingHandler answer http.NotFound for "/" → "GET / = 404"; make the loop // return the unchanged cfg on the first tick → "the box did not come up configured". func TestWaitForConfig_ServesWaitingPageThenStarts(t *testing.T) { dir := t.TempDir() _, cfgPath := writeBootstrap(t, dir, goodBootstrapV2) var calls atomic.Int32 pull := func(_, _, _ string) (string, error) { if calls.Add(1) <= 5 { return "", errors.New("hub answered 503") // permanent: no in-call retry, the loop retries } return hubYAML, nil } ln, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } addr := "http://" + ln.Addr().String() // The first ingestion failed: the caller holds an unconfigured config. start := MaybeIngest(cfgPath, config.Default(), testLogger(), pull) if !Unconfigured(start) { t.Fatal("setup: the first ingestion should have failed") } done := make(chan *config.Config, 1) go func() { done <- WaitForConfig(context.Background(), ln, cfgPath, start, testLogger(), pull, 150*time.Millisecond, "0.308.0") }() get := func(path string) (int, string) { t.Helper() resp, err := http.Get(addr + path) if err != nil { t.Fatalf("GET %s: %v", path, err) } defer resp.Body.Close() b, _ := io.ReadAll(resp.Body) return resp.StatusCode, string(b) } code, body := get("/") if code != http.StatusOK { t.Fatalf("GET / = %d — an unconfigured box must show the waiting page, not a 404", code) } for _, want := range []string{`lang="hu"`, "nem érte el a központot", `lang="en"`, "has not reached the central service yet", "support@felhom.eu"} { if !strings.Contains(body, want) { t.Errorf("the waiting page does not carry %q", want) } } if strings.Contains(strings.ToLower(body), "varázsló") || strings.Contains(body, "8081") { t.Error("the waiting page still points at the setup wizard") } if code, _ := get("/backups/restore"); code != http.StatusOK { t.Errorf("GET /backups/restore = %d — every path shows the waiting page", code) } if code, body := get("/api/health"); code != http.StatusOK || !strings.Contains(body, `"waiting_for_hub":true`) { t.Errorf("health while waiting: %d %s", code, body) } select { case got := <-done: if got == nil || Unconfigured(got) || got.Customer.ID != "cust-8200" { t.Fatalf("the box did not come up configured: %+v", got) } case <-time.After(10 * time.Second): t.Fatal("WaitForConfig never returned although the hub answered") } if _, err := http.Get(addr + "/"); err == nil { t.Error("the waiting page is still served after the box was configured") } } // A stop signal while waiting returns nil (the process exits); it never pretends to be configured. func TestWaitForConfig_StopReturnsNil(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() if got := WaitForConfig(ctx, nil, "", config.Default(), testLogger(), nil, time.Hour, "x"); got != nil { t.Fatalf("a stopped wait returned a config: %+v", got) } }