package api import ( "encoding/json" "io" "log" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/config" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" ) // R-619: GET /api/stacks//deploy-fields served a `type: password` field as `required:false` (as the // template declares), while the deploy refuses 400 without it — so a caller that trusted the contract // was refused (measured on grafana, 2026-09-21). The consequence asserted, on the wire through the real // handler: the password field arrives `required:true`; a `secret` field (which the box DOES generate) // keeps its declared `required:false`; and the stack's own metadata is not changed for the next reader. func TestR619_PasswordFieldIsServedAsRequired(t *testing.T) { dir := t.TempDir() cfg := &config.Config{} cfg.Paths.StacksDir = filepath.Join(dir, "stacks") cfg.Stacks.ComposeCommand = "docker compose" app := filepath.Join(cfg.Paths.StacksDir, "grafana") if err := os.MkdirAll(app, 0o755); err != nil { t.Fatal(err) } _ = os.WriteFile(filepath.Join(app, "docker-compose.yml"), []byte("services:\n grafana:\n image: busybox\n"), 0o644) _ = os.WriteFile(filepath.Join(app, ".felhom.yml"), []byte(`display_name: Grafana deploy_fields: - env_var: GF_SECURITY_ADMIN_PASSWORD label: Admin jelszo type: password generate: "password:16" required: false - env_var: GF_SECRET_KEY label: Titkos kulcs type: secret generate: "hex:32" required: false `), 0o644) m, err := stacks.NewManager(cfg, log.New(io.Discard, "", 0)) if err != nil { t.Fatal(err) } if err := m.ScanStacks(); err != nil { t.Fatal(err) } r := &Router{stackMgr: m, cfg: cfg, logger: log.New(io.Discard, "", 0)} read := func() map[string]bool { w := httptest.NewRecorder() r.getDeployFields(w, httptest.NewRequest(http.MethodGet, "/api/stacks/grafana/deploy-fields", nil), "grafana") if w.Code != http.StatusOK { t.Fatalf("status %d: %s", w.Code, w.Body.String()) } var body struct { Data struct { Metadata struct { DeployFields []struct { EnvVar string `json:"env_var"` Required bool `json:"required"` } `json:"deploy_fields"` } `json:"metadata"` } `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil { t.Fatal(err) } out := map[string]bool{} for _, f := range body.Data.Metadata.DeployFields { out[f.EnvVar] = f.Required } return out } got := read() if !got["GF_SECURITY_ADMIN_PASSWORD"] { t.Errorf("R-619: the password field reaches the wire as required:false, but the deploy refuses without it") } if req, ok := got["GF_SECRET_KEY"]; !ok || req { t.Errorf("a secret field (the box generates it) must keep its declared required:false; got present=%v required=%v", ok, req) } if meta, _, _ := m.GetDeployFields("grafana"); meta.DeployFields[0].Required { t.Errorf("the derivation leaked into the stack's metadata — it must be applied to the answer only") } }