f30a80841e
gates / gates (push) Successful in 50s
FileBrowser (and traefik, cloudflared, samba) has no app.yaml, so Deployed is false. Since v0.315.0 the app page read Deployed alone and showed FileBrowser "not installed", with Install and without its login. A protected infra stack is now installed by definition on the app page (Deployed || Protected, as every other surface already counted): settings/guide/log tabs, Restart only, no install page, no data tab. The Install press would have deployed the infra stack as an app: DeployStack and the deploy API now refuse a protected stack. In the operator state (the box does not know FileBrowser's password - both demo boxes) the login panel offers "Új jelszó beállítása": the file browser's own CLI with the server stopped (spiked on demo-hp: new 200, old 401, users/shares/settings unchanged), verified, stored encrypted, state generated. Every installed app's settings tab has the login panel with exactly one answer: the box's fields, the login the app wrote (InitialCreds), or a sentence chosen by the catalog's new app_info.login_kind (first_visit default, email, none; none behind the family gate says the family sign-in is the login). "Fájlok ↗" opens the library folder, else the first declared folder, else a media app's first present drive's media folder. The data tab lists each drive's media folders; a folder whose drive is not bound into FileBrowser is text, never a link. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
105 lines
3.9 KiB
Go
105 lines
3.9 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/crypto"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
)
|
|
|
|
// R-513 — the file manager's login is shown where app logins are, under the R-254 rule: the value is
|
|
// never in the page, only behind the reveal act.
|
|
|
|
const fbTestPW = "TESTONLYfbPw7Kq2"
|
|
|
|
func renderFBPage(t *testing.T, operatorSet bool) string {
|
|
t.Helper()
|
|
s := securityHarness(t)
|
|
s.loadTemplates()
|
|
data := map[string]interface{}{
|
|
"Page": "stacks", "Title": "FileBrowser", "Domain": "example.hu",
|
|
"Stack": stacks.Stack{Name: "filebrowser", Deployed: true, State: "running"},
|
|
"Meta": stacks.Metadata{DisplayName: "FileBrowser", Slug: "filebrowser"},
|
|
"HasAppInfo": true,
|
|
"InitialCreds": &stacks.ExtractedCreds{Available: true, Username: "admin"},
|
|
}
|
|
if operatorSet {
|
|
data["InitialCredsOperatorSet"] = true
|
|
} else {
|
|
data["InitialCredsHasPassword"] = true
|
|
}
|
|
meta := data["Meta"].(stacks.Metadata)
|
|
data["Infra"] = true
|
|
data["LoginAnswer"] = loginAnswerFor(data, &meta) // v0.320.0: what the settings tab's handler sets
|
|
var buf bytes.Buffer
|
|
if err := s.tmpl.ExecuteTemplate(&buf, "app_settings", data); err != nil {
|
|
t.Fatalf("render: %v", err)
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func TestFileBrowserLoginCard_Generated(t *testing.T) {
|
|
html := renderFBPage(t, false)
|
|
if !strings.Contains(html, "initial-credentials/reveal") || !strings.Contains(html, "admin") {
|
|
t.Fatal("generated state: the login card has no reveal call or no username")
|
|
}
|
|
if strings.Contains(html, "data-fb-operator") || strings.Contains(html, "data-fb-reset") {
|
|
t.Fatal("generated state shows the operator-set text or the new-password button")
|
|
}
|
|
}
|
|
|
|
// RED-PROOF (run 2026-09-15, recorded in REPORT.md): with the {{if .InitialCredsOperatorSet}} branch
|
|
// removed from app_info.html, the operator page rendered the reveal button and this failed at
|
|
// "operator state still offers a reveal".
|
|
func TestFileBrowserLoginCard_OperatorSet(t *testing.T) {
|
|
html := renderFBPage(t, true)
|
|
// v0.320.0: the sentence is the household's („te allitottad be... a doboz nem ismeri") and the
|
|
// „new password" press sits under it.
|
|
if !strings.Contains(html, "data-fb-operator") || !strings.Contains(html, "doboz nem ismeri") {
|
|
t.Fatal("operator state does not say the box does not know the password")
|
|
}
|
|
if !strings.Contains(html, `data-fb-reset="1"`) || !strings.Contains(html, "/apps/filebrowser/initial-credentials/reset") {
|
|
t.Fatal("operator state offers no new-password button")
|
|
}
|
|
if strings.Contains(html, `id="initcred-reveal"`) {
|
|
t.Fatal("operator state still offers a reveal for a password the Felhom does not hold")
|
|
}
|
|
}
|
|
|
|
func revealFB(t *testing.T, s *Server) (int, map[string]any) {
|
|
t.Helper()
|
|
w := httptest.NewRecorder()
|
|
s.fileBrowserPasswordReveal(w, httptest.NewRequest(http.MethodPost, "/apps/filebrowser/initial-credentials/reveal", nil))
|
|
var body map[string]any
|
|
_ = json.Unmarshal(w.Body.Bytes(), &body)
|
|
return w.Code, body
|
|
}
|
|
|
|
func TestFileBrowserPasswordReveal(t *testing.T) {
|
|
s := securityHarness(t)
|
|
s.encKey = bytes.Repeat([]byte{7}, 32)
|
|
if code, _ := revealFB(t, s); code != http.StatusNotFound {
|
|
t.Fatalf("undecided: want 404, got %d", code)
|
|
}
|
|
_ = s.settings.SetFileBrowserAdmin(settings.FileBrowserAdminOperator, "", "2026-09-15T00:00:00Z")
|
|
if code, _ := revealFB(t, s); code != http.StatusNotFound {
|
|
t.Fatalf("operator-set: want 404, got %d", code)
|
|
}
|
|
enc, err := crypto.Encrypt(s.encKey, fbTestPW)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = s.settings.SetFileBrowserAdmin(settings.FileBrowserAdminGenerated, enc, "2026-09-15T00:00:00Z")
|
|
code, body := revealFB(t, s)
|
|
data, _ := body["data"].(map[string]any)
|
|
if code != http.StatusOK || data["password"] != fbTestPW {
|
|
t.Fatalf("generated: want 200 + the decrypted password, got %d %v", code, body)
|
|
}
|
|
}
|