package stacks import ( "encoding/json" "io" "log" "net/http" "net/http/httptest" "path/filepath" "sync" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/crypto" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" ) // R-513 — FileBrowser accepted admin/admin on every box. The fake below behaves like the measured // Quantum 1.3.3 API (evidence-p1fixes-2026-09-15/B1): login by X-Password, PUT /api/users with the // current password in X-Password. type fakeFB struct { mu sync.Mutex password string puts int } func (f *fakeFB) handler() http.Handler { mux := http.NewServeMux() mux.HandleFunc("/api/auth/login", func(w http.ResponseWriter, r *http.Request) { f.mu.Lock() defer f.mu.Unlock() if r.Header.Get("X-Password") != f.password { w.WriteHeader(http.StatusUnauthorized) return } io.WriteString(w, "tok-123") }) mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) { f.mu.Lock() defer f.mu.Unlock() if r.Header.Get("X-Auth") != "tok-123" { w.WriteHeader(http.StatusUnauthorized) return } switch r.Method { case http.MethodGet: io.WriteString(w, `{"id":1,"username":"admin"}`) case http.MethodPut: if r.Header.Get("X-Password") != f.password { w.WriteHeader(http.StatusUnauthorized) return } var body struct { Which []string `json:"which"` Data struct { Password string `json:"password"` } `json:"data"` } _ = json.NewDecoder(r.Body).Decode(&body) f.password = body.Data.Password f.puts++ w.WriteHeader(http.StatusNoContent) } }) return mux } func fbManager(t *testing.T, base string) (*Manager, *settings.Settings, []byte) { t.Helper() st, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), log.New(io.Discard, "", 0)) if err != nil { t.Fatal(err) } key := make([]byte, 32) for i := range key { key[i] = byte(i + 1) } return &Manager{logger: log.New(io.Discard, "", 0), settings: st, encKey: key, fbBaseURL: base}, st, key } // The consequence: after one tick admin/admin no longer logs in, the stored (decrypted) password // does, and the state is "generated". A second tick changes nothing. // // RED-PROOF (run 2026-09-15, recorded in REPORT.md): with EnsureFileBrowserAdminPassword returning // nil before the PUT, admin/admin stayed valid and this failed at "admin/admin still logs in". func TestFileBrowserAdmin_DefaultLoginReplaced(t *testing.T) { fb := &fakeFB{password: "admin"} srv := httptest.NewServer(fb.handler()) defer srv.Close() m, st, key := fbManager(t, srv.URL) if err := m.EnsureFileBrowserAdminPassword(); err != nil { t.Fatalf("ensure: %v", err) } if _, c, _ := fbLogin(srv.Client().Do, srv.URL, "admin"); c == http.StatusOK { t.Fatalf("admin/admin still logs in — R-513 not fixed") } state, enc, at := st.GetFileBrowserAdmin() if state != settings.FileBrowserAdminGenerated || enc == "" || at == "" { t.Fatalf("decision not recorded: state=%q enc=%v at=%q", state, enc != "", at) } if enc == fb.password { t.Fatal("the password was stored in plaintext") } pw, err := crypto.Decrypt(key, enc) if err != nil || len(pw) != 16 { t.Fatalf("stored password does not decrypt to a 16-char value (len=%d err=%v)", len(pw), err) } if _, c, _ := fbLogin(srv.Client().Do, srv.URL, pw); c != http.StatusOK { t.Fatalf("the stored password does not log in (HTTP %d)", c) } _ = m.EnsureFileBrowserAdminPassword() if fb.puts != 1 { t.Fatalf("a recorded decision was acted on again (puts=%d)", fb.puts) } } // A password set by hand (admin/admin refused) is NEVER overwritten. func TestFileBrowserAdmin_HandSetPasswordLeftAlone(t *testing.T) { fb := &fakeFB{password: "operator-set-by-hand"} srv := httptest.NewServer(fb.handler()) defer srv.Close() m, st, _ := fbManager(t, srv.URL) if err := m.EnsureFileBrowserAdminPassword(); err != nil { t.Fatal(err) } if fb.puts != 0 || fb.password != "operator-set-by-hand" { t.Fatalf("hand-set password was changed (puts=%d)", fb.puts) } if state, enc, _ := st.GetFileBrowserAdmin(); state != settings.FileBrowserAdminOperator || enc != "" { t.Fatalf("want state operator with no stored value, got %q enc=%v", state, enc != "") } } // FileBrowser not reachable → nothing recorded, so the next tick tries again. func TestFileBrowserAdmin_UnreachableRecordsNothing(t *testing.T) { srv := httptest.NewServer(http.NotFoundHandler()) url := srv.URL srv.Close() m, st, _ := fbManager(t, url) if err := m.EnsureFileBrowserAdminPassword(); err == nil { t.Fatal("want an error (for the log) when FileBrowser is unreachable") } if state, _, _ := st.GetFileBrowserAdmin(); state != "" { t.Fatalf("an unreachable FileBrowser recorded a decision: %q", state) } }