Files
felhom-controller/controller/internal/stacks/signup_block_test.go
T

119 lines
5.0 KiB
Go

package stacks
import (
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// v0.281.0 (`09` §3 decision 47) — sign-up closed once the first admin exists, and the household's 15 minutes.
const signupYml = "display_name: Gated App\nsetup_gate: true\nsignup_block: \"PathPrefix(`/signup`)\"\n" +
"deploy_fields:\n - env_var: DOMAIN\n type: domain\n - env_var: SUBDOMAIN\n type: subdomain\n default: gapp\n"
// Opening the gate puts the sign-up block up FIRST; then the gate comes down.
// COMPANION RED-PROOF: drop the writeSignupBlock call in OpenSetupGate → "no sign-up block after the gate opened"
// fails (between the open and the next tick a stranger could sign up).
func TestSignupBlock_TheGateOpensOnlyWithTheBlockUp(t *testing.T) {
m := gateManager(t, signupYml)
closedGate(t, m)
must(t, m.OpenSetupGate("gapp", SetupGateByHousehold))
b, err := os.ReadFile(m.signupBlockPath("gapp"))
if err != nil {
t.Fatal("no sign-up block after the gate opened")
}
for _, want := range []string{"Host(`gapp.example.hu`)", "PathPrefix(`/signup`)", `path: "/__felhom_gate/signup-closed"`, "http://felhom-controller:8080"} {
if !strings.Contains(string(b), want) {
t.Errorf("block lacks %q:\n%s", want, b)
}
}
if blocked, _ := m.SignupBlocked("gapp"); !blocked {
t.Fatal("SignupBlocked says open")
}
// Still gated → no block (the household may need the sign-up address for the first admin).
m2 := gateManager(t, signupYml)
closedGate(t, m2)
m2.SetupGateTick()
if _, err := os.Stat(m2.signupBlockPath("gapp")); !os.IsNotExist(err) {
t.Fatal("a block stands while the gate is still closed — the household could not sign up as the first admin")
}
}
// A block that cannot be written keeps the gate CLOSED.
// COMPANION RED-PROOF: ignore writeSignupBlock's error in OpenSetupGate → the gate opens with sign-up wide open.
func TestSignupBlock_UnwritableBlockKeepsTheGateClosed(t *testing.T) {
m := gateManager(t, signupYml)
dir := closedGate(t, m)
must(t, os.MkdirAll(m.signupBlockPath("gapp"), 0o755)) // a DIRECTORY where the file must go: rename fails
must(t, os.WriteFile(filepath.Join(m.signupBlockPath("gapp"), "x"), []byte("x"), 0o644))
if err := m.OpenSetupGate("gapp", SetupGateByHousehold); err == nil {
t.Fatal("the gate opened although the sign-up block could not be written")
}
if c := LoadAppConfig(dir); !c.SetupGate.Closed() {
t.Fatal("record says open")
}
if _, err := os.Stat(m.setupGatePath("gapp")); err != nil {
t.Fatal("the gate file was removed")
}
}
// The household's window lifts the block for 15 minutes; the loop puts it back after.
// COMPANION RED-PROOF: make signupWindowOpen always true → "the block did not come back" fails.
func TestSignupBlock_TheWindowOpensAndCloses(t *testing.T) {
m := gateManager(t, signupYml)
closedGate(t, m)
must(t, m.OpenSetupGate("gapp", SetupGateByProbe))
until, err := m.OpenSignupWindow("gapp")
must(t, err)
if until == "" {
t.Fatal("no end time")
}
if _, err := os.Stat(m.signupBlockPath("gapp")); !os.IsNotExist(err) {
t.Fatal("the window opened but the block is still up")
}
m.SetupGateTick()
if _, err := os.Stat(m.signupBlockPath("gapp")); !os.IsNotExist(err) {
t.Fatal("a tick inside the window put the block back")
}
if blocked, u := m.SignupBlocked("gapp"); blocked || u != until {
t.Fatalf("inside the window: blocked=%v until=%q", blocked, u)
}
later := time.Now().Add(signupWindow + time.Minute)
m.updateNowFn = func() time.Time { return later }
m.SetupGateTick()
if _, err := os.Stat(m.signupBlockPath("gapp")); err != nil {
t.Fatal("the window passed but the block did not come back")
}
}
// An app with no gate record (installed before, or on a box that never gated it) never gets a block, even when its
// template has signup_block — Part 0's rule. A removed app's block goes.
// COMPANION RED-PROOF: drop the `g == nil ||` / State check in reconcileSignupBlocks → a block appears on an
// app nobody gated.
func TestSignupBlock_NeverOnAnAppThisBoxDidNotGate(t *testing.T) {
m := gateManager(t, signupYml)
dir := filepath.Join(m.cfg.Paths.StacksDir, "gapp")
cfg := &AppConfig{Deployed: true, Env: map[string]string{"DOMAIN": "example.hu", "SUBDOMAIN": "gapp"}}
must(t, SaveAppConfig(dir, cfg, m.encKey, nil))
m.mu.Lock()
m.stacks["gapp"].Deployed, m.stacks["gapp"].State, m.stacks["gapp"].AppConfig = true, StateRunning, cfg
m.mu.Unlock()
must(t, os.MkdirAll(m.setupGateDir(), 0o755))
must(t, os.WriteFile(filepath.Join(m.setupGateDir(), "signup-block-gone.yml"), []byte("x"), 0o644))
m.SetupGateTick()
if _, err := os.Stat(m.signupBlockPath("gapp")); !os.IsNotExist(err) {
t.Fatal("a sign-up block appeared on an app this box never gated")
}
if _, err := os.Stat(m.setupGatePath("gapp")); !os.IsNotExist(err) {
t.Fatal("a catalog setup_gate closed an app that was already installed")
}
if _, err := os.Stat(filepath.Join(m.setupGateDir(), "signup-block-gone.yml")); !os.IsNotExist(err) {
t.Fatal("a block nobody owns was kept")
}
if _, err := m.OpenSignupWindow("gapp"); err != ErrNoSignupBlock {
t.Fatalf("window on an ungated app: %v", err)
}
}