1453cfc69b
gates / gates (push) Failing after 50s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
90 lines
3.0 KiB
Go
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")
|
|
}
|
|
}
|