package bootstrap import ( "os" "strings" "testing" ) // RefreshConfig re-pulls the hub yaml and OVERWRITES controller.yaml, re-merging local_api from // bootstrap.json. Unlike MaybeIngest it is not first-boot-gated — it clobbers an existing config (the // hub is the source of truth for controller.yaml). func TestRefreshConfig_RePullsAndMerges(t *testing.T) { dir := t.TempDir() _, cfgPath := writeBootstrap(t, dir, goodBootstrapV2) // Pre-existing (stale) controller.yaml that must be overwritten. if err := os.WriteFile(cfgPath, []byte("customer:\n id: cust-8200\n name: OLD\n"), 0o600); err != nil { t.Fatal(err) } var calls int var gotID, gotPass string pull := func(hubURL, customerID, pass string) (string, error) { calls++ gotID, gotPass = customerID, pass return hubYAML, nil } if err := RefreshConfig(cfgPath, testLogger(), pull); err != nil { t.Fatalf("RefreshConfig: %v", err) } if calls != 1 { t.Errorf("pull called %d times, want 1", calls) } if gotID != "cust-8200" || gotPass != "five-word-passphrase-here" { t.Errorf("pull args = (%q,%q), want (cust-8200, five-word-passphrase-here)", gotID, gotPass) } out, err := os.ReadFile(cfgPath) if err != nil { t.Fatal(err) } s := string(out) if strings.Contains(s, "OLD") { t.Errorf("controller.yaml still contains stale content:\n%s", s) } if !strings.Contains(s, "CUSTKEY_FROM_HUB") { t.Errorf("controller.yaml missing hub-pulled api_key:\n%s", s) } // local_api re-merged from bootstrap.json (the hub yaml lacks it). if !strings.Contains(s, "PERGUESTTOKEN") || !strings.Contains(s, "192.168.0.162:8443") { t.Errorf("controller.yaml missing re-merged local_api:\n%s", s) } } // Fail-safe: a failed pull leaves the existing controller.yaml UNCHANGED and returns an error (the // caller then keeps the current config and does not restart). func TestRefreshConfig_FailedPullLeavesConfigUnchanged(t *testing.T) { dir := t.TempDir() _, cfgPath := writeBootstrap(t, dir, goodBootstrapV2) original := "customer:\n id: cust-8200\n name: CURRENT\n" if err := os.WriteFile(cfgPath, []byte(original), 0o600); err != nil { t.Fatal(err) } pull := func(hubURL, customerID, pass string) (string, error) { return "", ErrPullTransient // hub unreachable } if err := RefreshConfig(cfgPath, testLogger(), pull); err == nil { t.Fatal("RefreshConfig should return an error on a failed pull") } out, _ := os.ReadFile(cfgPath) if string(out) != original { t.Errorf("controller.yaml changed despite failed pull:\n%s", string(out)) } } // Fail-safe: no bootstrap.json (e.g. a wizard-configured guest) → error, nothing written. func TestRefreshConfig_NoBootstrapErrors(t *testing.T) { dir := t.TempDir() cfgPath := dir + "/controller.yaml" t.Setenv("FELHOM_BOOTSTRAP_PATH", dir+"/does-not-exist.json") called := false pull := func(hubURL, customerID, pass string) (string, error) { called = true; return hubYAML, nil } if err := RefreshConfig(cfgPath, testLogger(), pull); err == nil { t.Fatal("RefreshConfig should error when bootstrap.json is absent") } if called { t.Error("pull should not be called when bootstrap.json is absent") } if _, err := os.Stat(cfgPath); !os.IsNotExist(err) { t.Error("controller.yaml should not have been written") } }