7fa8768cfd
gates / gates (push) Successful in 25s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
95 lines
4.3 KiB
Go
95 lines
4.3 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
)
|
|
|
|
// R-709 (v0.280.0) — an installed app's `type: password` value is never in its settings page's HTML; it is
|
|
// fetched on demand, like a `type: secret` (R-254). The pre-deploy form keeps its generator.
|
|
|
|
const testAdminPassword = "TESTONLY-admin-pw-Qz72"
|
|
|
|
func renderDeployWithPassword(t *testing.T, alreadyDeployed bool, restored bool) string {
|
|
t.Helper()
|
|
s := securityHarness(t)
|
|
s.loadTemplates()
|
|
data := map[string]interface{}{
|
|
"Page": "stacks", "Title": "Telepítés", "Domain": "example.hu",
|
|
"Stack": stacks.Stack{Name: "grafana", Deployed: alreadyDeployed},
|
|
"Meta": stacks.Metadata{DisplayName: "Grafana", Slug: "grafana"},
|
|
"AlreadyDeployed": alreadyDeployed,
|
|
"UserFields": []stacks.DeployField{
|
|
{EnvVar: "ADMIN_PASSWORD", Label: "Admin jelszó", Type: "password", Generate: "password:24:special"},
|
|
},
|
|
// The PRE-FIX handler shape: the stored value in DeployedFieldValues. The template must not print it
|
|
// even then (the handler now also drops it — TestR709_TheRevealEndpointServesAnInstalledPassword).
|
|
"DeployedFieldValues": map[string]string{"ADMIN_PASSWORD": testAdminPassword},
|
|
"RestoredLogins": map[string]bool{"ADMIN_PASSWORD": restored},
|
|
}
|
|
var buf bytes.Buffer
|
|
if err := s.tmpl.ExecuteTemplate(&buf, "deploy", data); err != nil {
|
|
t.Fatalf("render deploy: %v", err)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
// COMPANION RED-PROOF: put `value="{{index $.DeployedFieldValues .EnvVar}}"` back on the deployed password input
|
|
// → the first assertion fails with the password in the body.
|
|
func TestR709_AnInstalledAppsPasswordIsNotInThePage(t *testing.T) {
|
|
html := renderDeployWithPassword(t, true, false)
|
|
if strings.Contains(html, testAdminPassword) {
|
|
t.Fatal("R-709: the installed app's admin password is in the HTML of its settings page")
|
|
}
|
|
if !strings.Contains(html, `onclick="revealPasswordField('grafana', 'ADMIN_PASSWORD', this)"`) {
|
|
t.Fatal("no reveal control: the household cannot see its own password")
|
|
}
|
|
// A login a restore generated is not the app's login (R-694): no reveal for it.
|
|
if html := renderDeployWithPassword(t, true, true); strings.Contains(html, `revealPasswordField('grafana'`) || strings.Contains(html, testAdminPassword) {
|
|
t.Fatal("a restore-generated login is offered for reveal")
|
|
}
|
|
// The pre-deploy form keeps its generator, with the field's own spec.
|
|
if html := renderDeployWithPassword(t, false, false); !strings.Contains(html, `generatePassword('field-ADMIN_PASSWORD', 'field-confirm-ADMIN_PASSWORD', 'password:24:special')`) {
|
|
t.Fatal("the install form lost its generator (or its spec)")
|
|
}
|
|
}
|
|
|
|
// The reveal endpoint serves an installed app's password field, refuses a restore-generated one and an unknown
|
|
// field. COMPANION RED-PROOF: drop the new password branch in appAutoFieldRevealHandler → the first case 403s.
|
|
func TestR709_TheRevealEndpointServesAnInstalledPassword(t *testing.T) {
|
|
s := gateHarness(t)
|
|
app := filepath.Join(s.cfg.Paths.StacksDir, "gapp")
|
|
if err := os.WriteFile(filepath.Join(app, ".felhom.yml"), []byte("display_name: G\nslug: gapp\ndeploy_fields:\n - env_var: ADMIN_PASSWORD\n type: password\n - env_var: OLD_PW\n type: password\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := stacks.SaveAppConfig(app, &stacks.AppConfig{Deployed: true, Env: map[string]string{"ADMIN_PASSWORD": testAdminPassword, "OLD_PW": "x"}, RestoredLogins: []string{"OLD_PW"}}, nil, nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.stackMgr.ScanStacks(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ask := func(field string) *httptest.ResponseRecorder {
|
|
r := httptest.NewRequest(http.MethodPost, "/stacks/gapp/auto-field/reveal", strings.NewReader("env_var="+field))
|
|
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
w := httptest.NewRecorder()
|
|
s.appAutoFieldRevealHandler(w, r, "gapp")
|
|
return w
|
|
}
|
|
if w := ask("ADMIN_PASSWORD"); w.Code != http.StatusOK || !strings.Contains(w.Body.String(), testAdminPassword) {
|
|
t.Fatalf("installed password: %d %s", w.Code, w.Body.String())
|
|
}
|
|
if w := ask("OLD_PW"); w.Code != http.StatusForbidden {
|
|
t.Fatalf("a restore-generated login was revealed: %d", w.Code)
|
|
}
|
|
if w := ask("NOPE"); w.Code != http.StatusForbidden {
|
|
t.Fatalf("an unknown field: %d", w.Code)
|
|
}
|
|
}
|