v0.297.0: burn-down round 2 — 24 small rows (R-591 R-568 R-567 R-363 R-547 R-10 R-552 R-251 R-104 R-619 R-362 R-675 R-256 R-257 R-240 R-365 R-425 R-565 R-564 R-603 R-454 R-208 R-457-swept) + the banner countdown and deepCopyStack twins; MinAgent 0.131.0
gates / gates (push) Failing after 50s
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
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user