Files
felhom-controller/controller/internal/api/r619_password_required_test.go
T

90 lines
3.0 KiB
Go

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/<n>/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")
}
}