v0.281.0: 'Done' asks the probe first; sign-up closed after the first admin (decision 47); R-713 code-bound values refused, ${NAME|base64}
gates / gates (push) Successful in 23s
gates / gates (push) Successful in 23s
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,105 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
)
|
||||
|
||||
// v0.281.0 — the household's "Done" press asks the app first (Part A), and the sign-up block's answer (decision 47).
|
||||
|
||||
func pressDone(s *Server) *httptest.ResponseRecorder {
|
||||
r := httptest.NewRequest(http.MethodPost, "/apps/gapp/setup-gate/open", nil)
|
||||
w := httptest.NewRecorder()
|
||||
s.appSetupGateOpenHandler(w, r, "gapp")
|
||||
return w
|
||||
}
|
||||
|
||||
// The uptime-kuma shape of 2026-09-29: a press BEFORE the setup. With a probe it is refused while the app says
|
||||
// "not done", and when the probe cannot be read; it opens once the app says "done".
|
||||
// COMPANION RED-PROOF: remove the SetupGateProbe check in appSetupGateOpenHandler → the first press opens the app.
|
||||
func TestSetupGateButton_RefusedWhileTheAppSaysNotDone(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: Gated App\nslug: gapp\nsetup_gate: true\nsetup_done_probe:\n url: http://gapp:80/status\n field: isInit\n done: \"true\"\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := s.stackMgr.ScanStacks(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
answer, probeErr := `{"isInit":false}`, error(nil)
|
||||
defer stacks.SetSetupGateProbeGetForTest(func(string) ([]byte, error) { return []byte(answer), probeErr })()
|
||||
if w := pressDone(s); w.Code != http.StatusConflict || !strings.Contains(w.Body.String(), "még nincs kész") {
|
||||
t.Fatalf("press before the setup: %d %s — want 409 and the sentence", w.Code, w.Body.String())
|
||||
}
|
||||
if _, closed, _ := s.stackMgr.SetupGateHost("gapp.example.hu"); !closed {
|
||||
t.Fatal("the press before the setup opened the gate")
|
||||
}
|
||||
answer, probeErr = `{"isInit":true}`, errors.New("connection refused")
|
||||
if w := pressDone(s); w.Code != http.StatusConflict {
|
||||
t.Fatalf("an unreadable status: %d — want 409 (fail closed)", w.Code)
|
||||
}
|
||||
probeErr = nil
|
||||
if w := pressDone(s); w.Code != http.StatusOK {
|
||||
t.Fatalf("the app says done: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
if _, closed, _ := s.stackMgr.SetupGateHost("gapp.example.hu"); closed {
|
||||
t.Fatal("still closed after a press the app agreed with")
|
||||
}
|
||||
}
|
||||
|
||||
// The sign-up address answers "closed": a page for a browser, 403 JSON for anything else.
|
||||
func TestSignupClosed_TheAnswer(t *testing.T) {
|
||||
s := gateHarness(t)
|
||||
r := httptest.NewRequest(http.MethodGet, "https://gapp.example.hu"+signupClosedPath, nil)
|
||||
r.Host = "gapp.example.hu"
|
||||
r.Header.Set("Accept", "text/html")
|
||||
w := httptest.NewRecorder()
|
||||
s.ServeSignupClosed(w, r)
|
||||
if w.Code != http.StatusForbidden || !strings.Contains(w.Body.String(), "nem lehet regisztr") {
|
||||
t.Fatalf("browser: %d\n%s", w.Code, w.Body.String())
|
||||
}
|
||||
r = httptest.NewRequest(http.MethodPost, signupClosedPath, strings.NewReader(`{"username":"x"}`))
|
||||
w = httptest.NewRecorder()
|
||||
s.ServeSignupClosed(w, r)
|
||||
if w.Code != http.StatusForbidden || !strings.Contains(w.Body.String(), "sign-up is closed") {
|
||||
t.Fatalf("API: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
// No block on this app (it is still gated): the window press says so.
|
||||
req := httptest.NewRequest(http.MethodPost, "/apps/gapp/signup-window", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
s.appSignupWindowHandler(rec, req, "gapp")
|
||||
if rec.Code != http.StatusConflict {
|
||||
t.Fatalf("window on a gated app: %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// The page: a probe app now has the press too; an app without one asks first, in the page (felhomConfirm); the
|
||||
// sign-up card with the app's own "how to add a family member" and the window button.
|
||||
func TestSetupGatePage_ConfirmAndSignupCard(t *testing.T) {
|
||||
noProbe := renderAppInfoWith(t, map[string]interface{}{"SetupGateClosed": true})
|
||||
if !strings.Contains(noProbe, "felhomConfirm(b, 'Csak akkor nyomd meg") {
|
||||
t.Fatal("no-probe press without the in-page confirm")
|
||||
}
|
||||
probe := renderAppInfoWith(t, map[string]interface{}{"SetupGateClosed": true, "SetupGateHasProbe": true})
|
||||
if !strings.Contains(probe, "/apps/gapp/setup-gate/open") || strings.Contains(probe, "felhomConfirm(b, 'Csak akkor") {
|
||||
t.Fatal("probe app: want the press, without the confirm")
|
||||
}
|
||||
closed := renderAppInfoWith(t, map[string]interface{}{"SignupClosed": true, "AppInfo": stacks.AppInfo{Tagline: "t", AddPeople: "Beállítások → Felhasználók → Meghívás."}})
|
||||
if !strings.Contains(closed, `id="signup-card"`) || !strings.Contains(closed, "Meghívás") || !strings.Contains(closed, "/apps/gapp/signup-window") {
|
||||
t.Fatal("sign-up card: text, how-to or window button missing")
|
||||
}
|
||||
open := renderAppInfoWith(t, map[string]interface{}{"SignupOpenUntil": "14:05"})
|
||||
if !strings.Contains(open, "14:05") || strings.Contains(open, "/apps/gapp/signup-window") {
|
||||
t.Fatal("open window: want the end time, no button")
|
||||
}
|
||||
if none := renderAppInfoWith(t, nil); strings.Contains(none, `id="signup-card"`) {
|
||||
t.Fatal("a sign-up card on an app with no block")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user