corrupt_rebuild_access_test.go
Rama main · réplica de lectura del repositorio original.
package main
import (
"bytes"
"context"
"fmt"
"io"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"github.com/pingular/pingularity/internal/config"
"github.com/pingular/pingularity/internal/settings"
"github.com/pingular/pingularity/internal/store"
"github.com/pingular/pingularity/internal/web"
)
// What the daemon hands the store is the operator's -on-corrupt choice and
// nothing else: with no choice made, a damaged database stops the start and
// stays where it is; with "rebuild", the same file is set aside and the daemon
// comes back on an empty store. This is the whole path from the flag to the
// file, so a wire crossed here is a wire crossed in production.
func TestOnlyAnExplicitRebuildChoiceArmsTheSetAside(t *testing.T) {
ctx := context.Background()
for _, tc := range []struct {
name string
cfg config.Config
rebuild bool
}{
{"no choice made", config.Config{}, false},
{"refuse", config.Config{OnCorrupt: config.OnCorruptRefuse}, false},
{"rebuild", config.Config{OnCorrupt: config.OnCorruptRebuild}, true},
} {
t.Run(tc.name, func(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "pingularity.db")
st, err := store.Open(dbPath)
if err != nil {
t.Fatalf("open: %v", err)
}
if err := st.SetSetting(ctx, "thresh_down_mbps", "123.5"); err != nil {
t.Fatal(err)
}
st.Close()
tearTableRoot(t, dbPath, "pauses")
st2, err := store.Open(dbPath, openOptionsFor(tc.cfg)...)
if err == nil {
defer st2.Close()
}
aside, _ := filepath.Glob(dbPath + ".*.corrupt")
switch {
case tc.rebuild && err != nil:
t.Fatalf("-on-corrupt rebuild did not reach the store: %v", err)
case tc.rebuild && len(aside) == 0:
t.Fatal("-on-corrupt rebuild reached the store but nothing was set aside")
case !tc.rebuild && err == nil:
t.Fatal("the damaged database was opened without anyone asking for the rebuild")
case !tc.rebuild && len(aside) != 0:
t.Fatalf("the damaged database was set aside without anyone asking (%v)", aside)
}
})
}
}
// And the daemon's own start, which is where that choice is spent: a damaged
// database and no choice made stops it before it has a store, a listener or a
// worker - the file untouched behind it.
func TestStartRefusesADamagedDatabaseWhenNobodyAskedForARebuild(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "pingularity.db")
st, err := store.Open(dbPath)
if err != nil {
t.Fatalf("open: %v", err)
}
st.Close()
tearTableRoot(t, dbPath, "pauses")
p := &program{cfg: config.Config{DBPath: dbPath}}
if err := p.Start(nil); err == nil {
p.store.Close()
t.Fatal("the daemon started on a damaged database nobody asked it to replace")
}
if matches, _ := filepath.Glob(dbPath + ".*.corrupt"); len(matches) != 0 {
t.Fatalf("the daemon set the damaged database aside unasked (%v)", matches)
}
}
// An install that was reachable from the LAN behind a password, whose database
// is damaged, and whose access came from the command line rather than the
// store. The rebuild empties the store - the bcrypt hash goes with the file set
// aside - but the flag is still on the command line, so a boot re-seeds
// "network" over a store that now has nothing to check a visitor against: a box
// that answered 403 to the LAN yesterday serves it an unauthenticated
// dashboard, writes included. Not only at the start that rebuilt the store -
// that one knows - but at the next, which opens a healthy database and has
// nothing on the handle to say what happened, and at every one after. The flag
// loses all of them, and a reload in between changes nothing, until a login is
// set on the store.
func TestARebuiltStoreDoesNotComeBackOnTheNetworkWithoutItsLogin(t *testing.T) {
ctx := context.Background()
dbPath := filepath.Join(t.TempDir(), "pingularity.db")
// The install as it stood: a password, and network access chosen.
st, err := store.Open(dbPath)
if err != nil {
t.Fatalf("open: %v", err)
}
if err := st.SetSettings(ctx, map[string]string{
"auth_enabled": "1",
"auth_user": "admin",
"auth_hash": "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy",
"access_local_only": "0",
}); err != nil {
t.Fatalf("seed the install: %v", err)
}
st.Close()
tearTableRoot(t, dbPath, "pauses")
// Every start runs with the flags the unit or compose file has always had,
// and does with them what run() does.
flags := config.Config{DBPath: dbPath, Access: "network", AccessExplicit: true, OnCorrupt: config.OnCorruptRebuild}
boot := func(st *store.Store) (*settings.Controller, config.Config, bool) {
t.Helper()
holdStands, err := st.AccessHoldAfterRebuild(ctx)
if err != nil {
t.Fatalf("read the access hold: %v", err)
}
cfg, held := holdAccessLocalAfterRebuild(flags, holdStands)
set, err := settings.New(ctx, st, testDefaultsFor(cfg))
if err != nil {
t.Fatalf("settings: %v", err)
}
bootAccessDecision(t, cfg, st, set, false)
return set, cfg, held
}
// The start that rebuilds.
st2, err := store.Open(dbPath, store.RebuildOnCorruption())
if err != nil {
t.Fatalf("reopen after the fault: %v", err)
}
if matches, _ := filepath.Glob(dbPath + ".*.corrupt"); len(matches) == 0 {
st2.Close()
t.Fatal("fixture: the damaged database was not set aside")
}
set, _, held := boot(st2)
if set.AuthActive() {
st2.Close()
t.Fatal("fixture: the rebuilt store still has a password, so this proves nothing")
}
if !held || !set.AccessLocalOnly() {
st2.Close()
t.Fatal("a store rebuilt without its password came back reachable from the LAN: every request from off the machine is served, unauthenticated, including the writes that change what this daemon measures")
}
st2.Close()
// The restart after it: a healthy database, and the same empty store.
st3, err := store.Open(dbPath)
if err != nil {
t.Fatalf("restart: %v", err)
}
set, cfg, held := boot(st3)
if !held || !set.AccessLocalOnly() {
st3.Close()
t.Fatal("the restart after a rebuild put the store on the LAN with no password behind it: the hold lasted one start, so the open dashboard only arrived one restart later")
}
// A reload - a reload signal, a restored backup - runs the access sequence
// again on the config the boot settled on, and must not find the flag there.
if err := set.Reload(ctx); err != nil {
st3.Close()
t.Fatalf("reload: %v", err)
}
if _, err := reconcileAccess(ctx, cfg, set); err != nil {
st3.Close()
t.Fatalf("reconcile after the reload: %v", err)
}
if !set.AccessLocalOnly() {
st3.Close()
t.Fatal("a reload opened the network the boot held back, over a store with no login")
}
// A login set from this machine, then a restart: the flag decides again, and
// now there is a password behind what it decides.
if err := set.SetAuthPassword(ctx, "admin", authHash); err != nil {
st3.Close()
t.Fatalf("set a password: %v", err)
}
st3.Close()
st4, err := store.Open(dbPath)
if err != nil {
t.Fatalf("restart with a login: %v", err)
}
defer st4.Close()
set, _, held = boot(st4)
if !set.AuthActive() {
t.Fatal("fixture: the login did not survive the restart")
}
if held || set.AccessLocalOnly() {
t.Fatal("a store with a login set on it is still held to this machine: the operator did what the warning asked and the network never came back")
}
}
// The way back for an install that cannot reach its own loopback address to set
// a password - a bridged container with no shell, whose published port the hold
// refuses like any other peer - is `pingularity reset-auth` against the
// database. It releases the hold and says so, and the next start is an unclaimed
// install on the network, which is what running it asked for. On a store with
// no hold it says nothing about one.
func TestResetAuthReleasesTheHoldOnARebuiltStore(t *testing.T) {
ctx := context.Background()
dbPath := filepath.Join(t.TempDir(), "pingularity.db")
st, err := store.Open(dbPath)
if err != nil {
t.Fatalf("open: %v", err)
}
st.Close()
tearTableRoot(t, dbPath, "pauses")
st, err = store.Open(dbPath, store.RebuildOnCorruption())
if err != nil {
t.Fatalf("rebuild: %v", err)
}
st.Close()
out := runResetAuth(t, dbPath)
if !strings.Contains(out, "hold is released") {
t.Errorf("reset-auth released nothing it said, or said nothing of what it released - the operator does not learn that the next start answers the network with no login:\n%s", out)
}
st, err = store.Open(dbPath)
if err != nil {
t.Fatalf("restart: %v", err)
}
defer st.Close()
holdStands, err := st.AccessHoldAfterRebuild(ctx)
if err != nil || holdStands {
t.Fatalf("the hold survived reset-auth (held=%v, err=%v): a bridged container stays off its own published port with no route left to a password", holdStands, err)
}
cfg, held := holdAccessLocalAfterRebuild(config.Config{DBPath: dbPath, Access: "network", AccessExplicit: true}, holdStands)
set, err := settings.New(ctx, st, testDefaultsFor(cfg))
if err != nil {
t.Fatalf("settings: %v", err)
}
bootAccessDecision(t, cfg, st, set, false)
if held || set.AccessLocalOnly() {
t.Fatal("after reset-auth released the hold, -access network still did not open the network")
}
if out := runResetAuth(t, dbPath); strings.Contains(out, "hold") {
t.Errorf("reset-auth on a store with no hold talks about one:\n%s", out)
}
}
// runResetAuth runs the reset-auth command against dbPath and returns what it
// printed, the way an operator reads it.
func runResetAuth(t *testing.T, dbPath string) string {
t.Helper()
f, err := os.CreateTemp(t.TempDir(), "reset-auth")
if err != nil {
t.Fatal(err)
}
defer f.Close()
orig := os.Stdout
os.Stdout = f
err = resetAuthCmd([]string{"-db", dbPath})
os.Stdout = orig
if err != nil {
t.Fatalf("reset-auth: %v", err)
}
b, err := os.ReadFile(f.Name())
if err != nil {
t.Fatal(err)
}
return string(b)
}
// Network access the operator switches on from this machine's own Access tab is
// not the flag: it is chosen at the machine, where the login would have been
// set, the same boundary reset-auth stands on - and it ends the hold, in this
// process at once and at every start after. It has to END it rather than win
// over it, because a network choice found stored while the hold stands is not
// honoured (see the rollback test below), and the store can only tell this
// build's own choice apart by the hold being gone.
func TestNetworkAccessSwitchedOnAtTheMachineReleasesTheHold(t *testing.T) {
ctx := context.Background()
dbPath := rebuiltDatabase(t)
flags := config.Config{DBPath: dbPath, Access: "network", AccessExplicit: true, OnCorrupt: config.OnCorruptRebuild}
st, err := store.Open(dbPath)
if err != nil {
t.Fatalf("restart: %v", err)
}
p, set, heldWarns := bootLikeRun(t, flags, st)
srv := serverLikeRun(p, st, set)
if code, _ := asPeer(t, srv, lanPeer, "GET", "/api/status", ""); code != http.StatusForbidden {
st.Close()
t.Fatalf("fixture: the held store answered a LAN peer %d before anything was released", code)
}
// Nothing about access is stored on this store, so the boot must not say a
// stored network choice is being refused: the flag is seeded no further than
// the hold allows, and the warning names only what is really there.
for _, w := range heldWarns {
if strings.Contains(fmt.Sprint(w.args...), "stored_network_access") {
st.Close()
t.Fatalf("the held boot of a store with no stored access choice warns that one is not honoured: %s %v", w.msg, w.args)
}
}
// Holding needs no stored choice: the boot seeds loopback rather than seed
// the flag's network and store a correction over it.
if got, stored := settingsSnapshot(t, st)["access_local_only"]; stored {
st.Close()
t.Fatalf("the held boot stored an access choice (access_local_only = %q) it had no need to store", got)
}
// The Access tab, from the machine: switch network access on, no login.
if code, body := asPeer(t, srv, loopbackPeer, "POST", "/api/access", `{"local_only":false}`); code != http.StatusOK {
st.Close()
t.Fatalf("POST /api/access from the machine = %d %q", code, body)
}
if code, _ := asPeer(t, srv, lanPeer, "GET", "/api/status", ""); code != http.StatusOK {
st.Close()
t.Fatalf("network access switched on at the machine still refuses the LAN (%d) in the process that switched it on", code)
}
if held, err := st.AccessHoldAfterRebuild(ctx); err != nil || held {
st.Close()
t.Fatalf("switching network access on at the machine left the hold in the store (held=%v, err=%v): the next start sets the operator's own choice back to local-only", held, err)
}
st.Close()
st, err = store.Open(dbPath)
if err != nil {
t.Fatalf("restart: %v", err)
}
defer st.Close()
p, set, warns := bootLikeRun(t, flags, st)
if set.AccessLocalOnly() || web.AccessHold(p.accessHold.Load()) != web.AccessNotHeld {
t.Fatal("the start after the operator switched network access on at the machine took it back")
}
for _, w := range warns {
if strings.Contains(w.msg, "access held to this machine") {
t.Errorf("a start serving the network the operator chose still warns that it is holding it: %s", w.msg)
}
}
}
// The rollback. A store rebuilt by this build, run back on an older release that
// does not know the hold: with -access network that release answers the network
// with no login, as the release notes say, and one ordinary Save - which posts
// the scope the drawer shows, and which any visitor to that open dashboard can
// send - stores network access beside the hold. Upgraded again with the same
// flags, that stored choice used to open the store for good, with no login, no
// warning and /readyz 200. It was not made on this build while the hold stood
// (that would have released it), so it is not honoured: the hold stands, the
// Access tab is shown the local-only scope in force, and the warning says what
// happened. It is not overwritten at boot - main stores an access decision only
// from explicit input - and the operator's own next Save stores local-only.
func TestARebuiltStoreDoesNotTakeNetworkAccessARollbackStored(t *testing.T) {
for _, tc := range []struct {
name string
flags config.Config
}{
{"under -access network", config.Config{Access: "network", AccessExplicit: true, OnCorrupt: config.OnCorruptRebuild}},
// A unit that passes no -access at all: the stored choice is all that
// would open it.
{"with no -access at all", config.Config{OnCorrupt: config.OnCorruptRebuild}},
} {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
dbPath := rebuiltDatabase(t)
flags := tc.flags
flags.DBPath = dbPath
// The older release's Save, as it lands in the file.
st, err := store.Open(dbPath)
if err != nil {
t.Fatal(err)
}
if err := st.SetSetting(ctx, "access_local_only", "0"); err != nil {
t.Fatal(err)
}
st.Close()
st, err = store.Open(dbPath)
if err != nil {
t.Fatalf("re-upgrade: %v", err)
}
defer st.Close()
p, set, warns := bootLikeRun(t, flags, st)
if got := web.AccessHold(p.accessHold.Load()); got != web.AccessHeldAfterRebuild {
t.Fatalf("the re-upgraded start records the hold as %d, want held after rebuild: the network access an older release stored beside the hold is in force, with no login behind it", got)
}
if got := settingsSnapshot(t, st)["access_local_only"]; got != "0" {
t.Errorf("the boot wrote the stored access scope (access_local_only = %q): main persists access only from explicit input", got)
}
said := false
for _, w := range warns {
if strings.Contains(w.msg, "access held to this machine") && strings.Contains(fmt.Sprint(w.args...), "stored_network_access") {
said = true
}
}
if !said {
t.Errorf("nothing said the stored network access was set back and why; warnings: %v", warns)
}
srv := serverLikeRun(p, st, set)
if code, _ := asPeer(t, srv, lanPeer, "GET", "/", ""); code != http.StatusForbidden {
t.Errorf("a LAN peer got %d from GET / on the re-upgraded store, want 403", code)
}
// The machine's own Access tab is shown the scope in force, so a Save
// there echoes local-only and stores it.
if _, body := asPeer(t, srv, loopbackPeer, "GET", "/api/access", ""); !strings.Contains(body, `"local_only":true`) {
t.Errorf("the Access tab is shown network access the hold refuses: %s", body)
}
if code, body := asPeer(t, srv, loopbackPeer, "POST", "/api/access", `{"local_only":true}`); code != http.StatusOK {
t.Fatalf("a Save at the machine = %d %q", code, body)
}
if got := settingsSnapshot(t, st)["access_local_only"]; got != "1" {
t.Errorf("the operator's Save at the machine did not store the local-only scope it was shown (access_local_only = %q)", got)
}
if held, err := st.AccessHoldAfterRebuild(ctx); err != nil || !held {
t.Errorf("a Save echoing local-only released the hold (held=%v, err=%v): an echo is not a choice", held, err)
}
if code, _ := asPeer(t, srv, lanPeer, "POST", "/api/settings", `{"thresh_down_mbps":4242}`); code != http.StatusForbidden {
t.Errorf("a LAN peer's unauthenticated POST /api/settings answered %d, want 403", code)
}
if got := settingsSnapshot(t, st)["thresh_down_mbps"]; got == "4242" {
t.Error("a LAN peer's unauthenticated settings write landed on the re-upgraded store")
}
if code, body := asPeer(t, srv, lanPeer, "GET", "/readyz", ""); code != http.StatusServiceUnavailable || !strings.Contains(body, "held") {
t.Errorf("/readyz on the re-upgraded store = %d %q, want a 503 saying the network is held", code, body)
}
// And a reload that pulls a stored network choice in under the
// standing hold - a restore, a reload signal after an edit - gets the
// same answer, and says so again.
logged := &lockedLog{}
p.log = slog.New(slog.NewTextHandler(logged, nil))
if err := st.SetSetting(ctx, "access_local_only", "0"); err != nil {
t.Fatal(err)
}
if err := set.Reload(ctx); err != nil {
t.Fatalf("reload: %v", err)
}
if code, _ := asPeer(t, srv, lanPeer, "GET", "/api/status", ""); code != http.StatusForbidden {
t.Errorf("a reload that pulled in a stored network choice under the hold put it in force: a LAN peer got %d", code)
}
if got := web.AccessHold(p.accessHold.Load()); got != web.AccessHeldAfterRebuild {
t.Errorf("the reload records the hold as %d, want held after rebuild", got)
}
})
}
}
// And the transient read. A healthy install - a login, and network access that
// has only ever come from PINGULARITY_ACCESS, so nothing about access is stored -
// whose settings could not be read for a moment at boot. Whether the store
// carries the hold could not be read either, and the network is held for that
// moment, failing closed. But the store was never rebuilt: the load that finally
// reads its settings must give the flag its network back, store nothing the
// flag never stored, and never have said the store was rebuilt - an operator who
// believed that and ran reset-auth would clear a working password and open the
// LAN with no login at all. The same load on a store that does carry the hold
// holds it.
func TestAHoldThatCouldNotBeReadAtBootIsJudgedWhenSettingsLoad(t *testing.T) {
ctx := context.Background()
dead, cancel := context.WithCancel(ctx)
cancel()
flags := config.Config{Access: "network", AccessExplicit: true}
t.Run("a healthy install", func(t *testing.T) {
flags := flags
flags.DBPath = filepath.Join(t.TempDir(), "pingularity.db")
st, err := store.Open(flags.DBPath)
if err != nil {
t.Fatal(err)
}
defer st.Close()
if err := st.SetSettings(ctx, map[string]string{"auth_enabled": "1", "auth_user": "admin", "auth_hash": authHash}); err != nil {
t.Fatal(err)
}
p, set, srv, logs := bootWithUnreadableSettings(t, flags, st, dead)
// Neither could be read, and a network peer is told what every build said
// for that before there was a hold to read - not that local-only access is
// on, and not to pass the -access network the install was started with.
if code, body := asPeer(t, srv, lanPeer, "GET", "/api/status", ""); code != http.StatusServiceUnavailable || !strings.Contains(body, "settings could not be loaded") {
t.Fatalf("a LAN peer got %d %q while neither the settings nor the hold could be read, want the 503 saying the settings could not be loaded", code, body)
}
if err := set.Reload(ctx); err != nil {
t.Fatalf("the settings load that recovers: %v", err)
}
if got := web.AccessHold(p.accessHold.Load()); got != web.AccessNotHeld {
t.Fatalf("the load that read the settings still records a hold (%d) on a store that carries none: the LAN stays refused for the life of the process", got)
}
if set.AccessLocalOnly() {
t.Error("PINGULARITY_ACCESS=network is not in force once the settings read: the install's own network is still refused")
}
if _, stored := settingsSnapshot(t, st)["access_local_only"]; stored {
t.Error("the recovering load stored an access choice the flag never needed stored; dropping the flag later would no longer close the network")
}
// Unauthenticated from the LAN: the login answers now, not the hold.
if code, _ := asPeer(t, srv, lanPeer, "GET", "/api/status", ""); code != http.StatusUnauthorized {
t.Errorf("a LAN peer with no credentials got %d, want 401 from the login - the network is back and the password stands behind it", code)
}
if code, body := asPeer(t, srv, loopbackPeer, "GET", "/readyz", ""); code != http.StatusOK {
t.Errorf("/readyz = %d %q once the settings read on a store nothing holds, want 200", code, body)
}
if got := logs(); !strings.Contains(got, "could not read or release the access hold") {
t.Errorf("the boot that could not read the hold never said so:\n%s", got)
} else if strings.Contains(got, "rebuilt after the database was found damaged") {
t.Errorf("a store that was never rebuilt was said to be:\n%s", got)
} else if !strings.Contains(got, "nothing holds network access back") {
t.Errorf("the load that read the hold never said the network it had held is back:\n%s", got)
}
})
// A load after a healthy boot whose own read of the hold fails: the network
// is held for that load, nothing is written on the guess, and nothing claims
// a rebuild; the next load that reads it gives the network back.
t.Run("a later load that cannot read the hold", func(t *testing.T) {
flags := flags
flags.DBPath = filepath.Join(t.TempDir(), "pingularity.db")
st, err := store.Open(flags.DBPath)
if err != nil {
t.Fatal(err)
}
defer st.Close()
if err := st.SetSettings(ctx, map[string]string{"auth_enabled": "1", "auth_user": "admin", "auth_hash": authHash}); err != nil {
t.Fatal(err)
}
p, set, _ := bootLikeRun(t, flags, st)
srv := serverLikeRun(p, st, set)
if code, _ := asPeer(t, srv, lanPeer, "GET", "/api/status", ""); code != http.StatusUnauthorized {
t.Fatalf("fixture: the healthy boot answered a LAN peer %d, want 401 from its login", code)
}
before := fmt.Sprint(settingsSnapshot(t, st))
var said []warnCall
sink := func(msg string, args ...any) { said = append(said, warnCall{msg: msg, args: args}) }
p.applyExplicitAccess(dead, set, false, sink, sink)
if got := p.currentAccessHold(); got != web.AccessHeldUnread {
t.Fatalf("a load that could not read the hold records it as %d, want unread", got)
}
if code, _ := asPeer(t, srv, lanPeer, "GET", "/api/status", ""); code != http.StatusForbidden {
t.Errorf("a LAN peer got %d while the hold could not be read, want 403: the access decision failed open", code)
}
if code, body := asPeer(t, srv, lanPeer, "GET", "/readyz", ""); code != http.StatusServiceUnavailable || strings.Contains(body, "rebuilt after") {
t.Errorf("/readyz = %d %q while the hold could not be read, want a 503 that claims no rebuild", code, body)
}
if after := fmt.Sprint(settingsSnapshot(t, st)); after != before {
t.Errorf("a load that could not read the hold wrote to the store on the guess:\nbefore %s\nafter %s", before, after)
}
if fmt.Sprint(said) == "[]" {
t.Error("a load that could not read the hold said nothing about it")
}
// The flag is what that load withholds: the config the rest of the
// sequence runs with does not carry it.
if cfg := p.judgeAccessHold(dead, set, sink, sink); cfg.Access == "network" || cfg.AccessExplicit {
t.Errorf("a load that could not read the hold hands the rest of the access sequence -access network to reconcile (%+v)", cfg)
}
// And while local-only is chosen at the machine: -access network is not
// reconciled over that choice on the guess either.
if err := set.SetAccessLocalOnly(ctx, true); err != nil {
t.Fatal(err)
}
before = fmt.Sprint(settingsSnapshot(t, st))
p.applyExplicitAccess(dead, set, false, sink, sink)
if after := fmt.Sprint(settingsSnapshot(t, st)); after != before {
t.Errorf("a load that could not read the hold reconciled -access network over the stored choice on a guess:\nbefore %s\nafter %s", before, after)
}
p.applyExplicitAccess(ctx, set, false, sink, sink)
if code, _ := asPeer(t, srv, lanPeer, "GET", "/api/status", ""); code != http.StatusUnauthorized {
t.Errorf("a LAN peer got %d once the hold read again, want 401: the network did not come back", code)
}
})
t.Run("a rebuilt store", func(t *testing.T) {
flags := flags
flags.DBPath = rebuiltDatabase(t)
st, err := store.Open(flags.DBPath)
if err != nil {
t.Fatal(err)
}
defer st.Close()
p, set, srv, logs := bootWithUnreadableSettings(t, flags, st, dead)
if err := set.Reload(ctx); err != nil {
t.Fatalf("the settings load that recovers: %v", err)
}
if got := web.AccessHold(p.accessHold.Load()); got != web.AccessHeldAfterRebuild {
t.Fatalf("the load that read a rebuilt store records the hold as %d, want held after rebuild", got)
}
// The scope in force is local-only, whatever the flag seeded: the hold is
// enforced where requests are judged, and nothing was stored on the
// guess the unreadable boot could have made.
if _, body := asPeer(t, srv, loopbackPeer, "GET", "/api/access", ""); !strings.Contains(body, `"local_only":true`) {
t.Errorf("the load that read a rebuilt store put PINGULARITY_ACCESS=network in force over it: %s", body)
}
if _, stored := settingsSnapshot(t, st)["access_local_only"]; stored {
t.Error("the load that read a rebuilt store stored an access choice")
}
if code, _ := asPeer(t, srv, lanPeer, "GET", "/api/status", ""); code != http.StatusForbidden {
t.Errorf("a LAN peer got %d from the rebuilt store once its settings read, want 403", code)
}
if !strings.Contains(logs(), "access held to this machine") {
t.Error("the load that found the hold standing never said why the network is refused")
}
// And it names nothing it did not find: the network in force is the flag's
// seed, and no network choice is stored on this store.
if strings.Contains(logs(), "stored_network_access") {
t.Errorf("the load that found the hold standing says a stored network choice is not honoured, on a store with none stored:\n%s", logs())
}
})
}
// The hold read standing at boot is in force from that moment, not only from the
// first load that judges it. A boot whose settings then fail to load serves on
// the seed until a load recovers them, and that load puts the stored values in
// force - and counts the settings loaded - a moment before the hook that judges
// the hold runs on them. A store an older release was rolled back to can carry
// network access beside the hold, and with nothing recorded as held in that
// moment a network peer was served with no login: it could change the settings,
// or claim the dashboard with a password of its own. The load that recovers still
// says why the network is held - the seed found the hold, but it is not a load.
func TestARebuiltStoreStaysHeldThroughTheLoadThatRecoversItsSettings(t *testing.T) {
ctx := context.Background()
dead, cancel := context.WithCancel(ctx)
cancel()
for _, tc := range []struct {
name string
flags config.Config
}{
{"under -access network", config.Config{Access: "network", AccessExplicit: true, OnCorrupt: config.OnCorruptRebuild}},
{"with no -access at all", config.Config{OnCorrupt: config.OnCorruptRebuild}},
} {
t.Run(tc.name, func(t *testing.T) {
dbPath := rebuiltDatabase(t)
flags := tc.flags
flags.DBPath = dbPath
st, err := store.Open(dbPath)
if err != nil {
t.Fatal(err)
}
defer st.Close()
// The older release's Save, as it lands in the file.
if err := st.SetSetting(ctx, "access_local_only", "0"); err != nil {
t.Fatal(err)
}
// The hold reads; the settings load straight after it does not.
logged := &lockedLog{}
p := &program{cfg: flags, store: st, log: slog.New(slog.NewTextHandler(logged, nil))}
seed := p.accessSeedAtBoot(ctx, func(msg string, args ...any) { p.log.Warn(msg, args...) })
set, err := settings.New(dead, st, testDefaultsFor(seed))
if err == nil {
t.Fatal("fixture: the settings load at boot must fail")
}
srv := serverLikeRun(p, st, set)
// registerSettingsLoadedHook's own sequence, with a network peer's
// requests landing as it starts: the stored network access is in force,
// the settings count as loaded, and nothing has judged the hold on them.
var served []string
set.OnLoaded(func(firstLoad bool) {
for _, q := range []struct{ method, path, body string }{
{"GET", "/api/status", ""},
{"POST", "/api/settings", `{"thresh_down_mbps":4242}`},
{"POST", "/api/access", `{"auth_enabled":true,"username":"visitor","password":"not-the-operators-1","local_only":false}`},
} {
if code, _ := asPeer(t, srv, lanPeer, q.method, q.path, q.body); code != http.StatusForbidden {
served = append(served, fmt.Sprintf("%s %s answered %d", q.method, q.path, code))
}
}
p.applyExplicitAccess(ctx, set, firstLoad,
func(msg string, args ...any) { p.log.Warn(msg, args...) },
func(msg string, args ...any) { p.log.Info(msg, args...) })
})
if err := set.Reload(ctx); err != nil {
t.Fatalf("the settings load that recovers: %v", err)
}
if !set.Loaded() || set.AccessLocalOnly() {
t.Fatal("fixture: the recovering load did not put the stored network access in force, so this proves nothing")
}
if len(served) != 0 {
t.Fatalf("a network peer was served by a rebuilt store with no login in the moment its settings recovered: %v", served)
}
if all := settingsSnapshot(t, st); all["thresh_down_mbps"] == "4242" || all["auth_user"] == "visitor" {
t.Errorf("a network peer's write landed on the rebuilt store while its settings recovered: thresh_down_mbps=%q auth_user=%q", all["thresh_down_mbps"], all["auth_user"])
}
if got := web.AccessHold(p.accessHold.Load()); got != web.AccessHeldAfterRebuild {
t.Errorf("the recovering load records the hold as %d, want held after rebuild", got)
}
if n := strings.Count(logged.String(), "access held to this machine"); n != 1 {
t.Errorf("the load that recovered the settings said why the network is held %d times, want once:\n%s", n, logged.String())
}
})
}
}
// The warning that says why the network is held comes with each load that finds
// the hold standing after one that did not - not with every reload of a store
// already held, where it would bury the line that mattered, and not with the
// boot's seed, which finds the hold before any load has judged it. The warning
// that the hold could not be read comes once for each time that starts.
func TestTheHoldWarningsComeOnceForEachChange(t *testing.T) {
ctx := context.Background()
dead, cancel := context.WithCancel(ctx)
cancel()
dbPath := rebuiltDatabase(t)
st, err := store.Open(dbPath)
if err != nil {
t.Fatal(err)
}
defer st.Close()
// The rollback's Save: network access stored beside the hold, and no -access
// at all, so the stored choice is the only thing asking for the network.
if err := st.SetSetting(ctx, "access_local_only", "0"); err != nil {
t.Fatal(err)
}
p, set, warns := bootLikeRun(t, config.Config{DBPath: dbPath, OnCorrupt: config.OnCorruptRebuild}, st)
logged := &lockedLog{}
p.log = slog.New(slog.NewTextHandler(logged, nil))
sink := func(msg string, args ...any) { p.log.Warn(msg, args...) }
said := func(what string, wantHeld, wantUnread int) {
t.Helper()
held := strings.Count(logged.String(), "access held to this machine")
for _, w := range warns {
if strings.Contains(w.msg, "access held to this machine") {
held++
}
}
if held != wantHeld {
t.Errorf("after %s the held warning has been given %d times, want %d:\n%s", what, held, wantHeld, logged.String())
}
if unread := strings.Count(logged.String(), "could not read or release the access hold"); unread != wantUnread {
t.Errorf("after %s the unread warning has been given %d times, want %d:\n%s", what, unread, wantUnread, logged.String())
}
}
said("the boot", 1, 0)
if err := set.Reload(ctx); err != nil {
t.Fatal(err)
}
said("a reload signal on the held store", 1, 0)
p.applyExplicitAccess(dead, set, false, sink, sink)
p.applyExplicitAccess(dead, set, false, sink, sink)
said("two loads that could not read the hold", 1, 1)
if err := set.Reload(ctx); err != nil {
t.Fatal(err)
}
said("the load that read it standing again", 2, 1)
// Local-only stored at the machine: nothing asks for the network, so there is
// nothing to hold back...
if err := set.SetAccessLocalOnly(ctx, true); err != nil {
t.Fatal(err)
}
if err := set.Reload(ctx); err != nil {
t.Fatal(err)
}
if got := p.currentAccessHold(); got != web.AccessNotHeld {
t.Fatalf("fixture: with local-only stored and no -access the load records the hold as %d, want not held", got)
}
// ...until a restored backup brings network access back in under the hold.
if err := st.SetSetting(ctx, "access_local_only", "0"); err != nil {
t.Fatal(err)
}
if err := set.Reload(ctx); err != nil {
t.Fatal(err)
}
said("a load that found the hold standing after one that held nothing back", 3, 1)
}
// And the other direction, which is every ordinary start: a healthy store keeps
// the access the operator asked for. The flag is a container's way back onto its
// own published port, and it must not be spent on installs that never had a
// fault.
func TestAHealthyStoreKeepsTheAccessTheFlagAsksFor(t *testing.T) {
ctx := context.Background()
dbPath := filepath.Join(t.TempDir(), "pingularity.db")
st, err := store.Open(dbPath)
if err != nil {
t.Fatalf("open: %v", err)
}
defer st.Close()
if err := st.SetSetting(ctx, "access_local_only", "1"); err != nil {
t.Fatalf("seed a stored local-only: %v", err)
}
holdStands, err := st.AccessHoldAfterRebuild(ctx)
if err != nil {
t.Fatalf("read the access hold: %v", err)
}
cfg := config.Config{DBPath: dbPath, Access: "network", AccessExplicit: true}
cfg, held := holdAccessLocalAfterRebuild(cfg, holdStands)
if held {
t.Fatal("a healthy store had its access held to this machine: an install with no fault just lost the network")
}
set, err := settings.New(ctx, st, testDefaultsFor(cfg))
if err != nil {
t.Fatalf("settings: %v", err)
}
bootAccessDecision(t, cfg, st, set, false)
if set.AccessLocalOnly() {
t.Fatal("an explicit -access network no longer overrides a stored local-only, so a container that persisted local-only has no way back onto its published port")
}
}
// Wherever a document states the access rule - an explicitly passed -access /
// PINGULARITY_ACCESS wins at every start, which is how a container gets back
// onto its own published port - it has to name the store where it does not,
// and the way back from it. That store is the dangerous direction: an operator
// who reads the unqualified rule expects the flag to have decided, and what it
// would have decided is a dashboard on the LAN with no password behind it; one
// who reads of the hold but not of reset-auth has a bridged container that 403s
// its own port and no route to a password. A sentence that promises what the
// code no longer does is how the hold gets deleted by someone tidying up an
// inconsistency. docs/cli.md states the rule in a table row, so rows are read
// one at a time - the table as one paragraph would find the exception in the
// -on-corrupt row and excuse an -access row that never names it.
func TestTheDocsThatPromiseTheAccessFlagAlwaysWinsNameTheStoreItDoesNot(t *testing.T) {
for _, doc := range []string{filepath.Join("docs", "install.md"), filepath.Join("docs", "security-model.md"), filepath.Join("docs", "cli.md")} {
stated := 0
for _, unit := range docUnits(mustReadRepoFile(t, doc)) {
flat := flatDoc(unit)
if !strings.Contains(flat, "authoritative at every start") && !strings.Contains(flat, "re-asserts itself at every start") {
continue
}
stated++
for want, missing := range map[string]string{
"on-corrupt": "the store it is not - one rebuilt after the database was found damaged, which holds the network back because the password went with the old file",
"reset-auth": "reset-auth, the way back for a bridged container the hold keeps off its own published port",
"switched on": "network access switched on at the machine, the one network choice the hold gives way to",
} {
if !strings.Contains(flat, want) {
t.Errorf("%s tells an operator an explicit access mode wins at every start and never names %s:\n%s", doc, missing, flat)
}
}
}
if stated == 0 {
t.Errorf("%s no longer states the every-start access rule this test qualifies; if the rule moved, move the exception with it", doc)
}
}
}
// What a reader of the corruption notes and of the readiness docs acts on, held
// to what the daemon does: the rebuilt store is loopback-only even under -access
// network, restart after restart; reset-auth is how a container gets out of it;
// and /readyz answers 503 while it lasts. Each of these sentences has been
// deleted or reverted once with every test still green.
func TestTheDocsSayWhatARebuiltStoreDoes(t *testing.T) {
readme := flatDoc(mustReadRepoFile(t, "docs/install.md"))
i := strings.Index(readme, "`-on-corrupt rebuild` takes the other road")
if i < 0 {
t.Fatal("README.md's corruption notes have no `-on-corrupt rebuild` entry")
}
entry := readme[i:]
if j := strings.Index(entry, "What the daemon will *not* do"); j >= 0 {
entry = entry[:j]
}
for want, what := range map[string]string{
"loopback-only even if you passed `-access network`": "that the rebuilt store refuses the network the flag asks for",
"every start": "that the hold outlives the start that rebuilt the store",
"reset-auth": "the way back for a bridged container",
"/readyz": "that readiness reports it",
"Network access on there yourself releases the hold": "that switching network access on at the machine releases the hold - the one network choice it gives way to",
"a network setting stored by anything else does not": "that a network setting a rolled-back release stored does not open the store",
"then restart or reload": "that a new password takes effect once the daemon restarts or reloads",
} {
if !strings.Contains(entry, want) {
t.Errorf("README.md's -on-corrupt rebuild entry no longer says %s (%q):\n%s", what, want, entry)
}
}
metrics := flatDoc(mustReadRepoFile(t, filepath.Join("docs", "metrics.md")))
i = strings.Index(metrics, "`GET /readyz`")
if i < 0 {
t.Fatal("docs/metrics.md no longer documents /readyz")
}
readyz := metrics[i:]
if j := strings.Index(readyz, "`pingularity healthz"); j >= 0 {
readyz = readyz[:j]
}
for want, what := range map[string]string{
"on-corrupt rebuild": "that a store rebuilt after damage answers 503",
"reset-auth": "what ends the 503 a held network keeps answering",
"switched on from the machine itself": "that network access switched on at the machine ends it too",
"and the daemon restarted or reloaded": "that a password set on the store ends the hold once the daemon restarts or reloads",
"without claiming a rebuild": "that a hold that could not be read answers 503 without saying the store was rebuilt",
} {
if !strings.Contains(readyz, want) {
t.Errorf("docs/metrics.md's /readyz entry no longer says %s (%q):\n%s", what, want, readyz)
}
}
// The flag's own two descriptions - the docs/cli.md row and `pingularity
// help` - are where an operator choosing -on-corrupt rebuild reads what it
// costs, so both have to say the hold outlasts the start and what ends it.
var row string
for _, u := range docUnits(mustReadRepoFile(t, filepath.Join("docs", "cli.md"))) {
if strings.HasPrefix(u, "| `-on-corrupt` |") {
row = flatDoc(u)
}
}
help, _ := usageEntry(captureUsage(t), "-on-corrupt")
for name, text := range map[string]string{"docs/cli.md's -on-corrupt row": row, "the curated help's -on-corrupt entry": flatDoc(help)} {
if !strings.Contains(text, "reset-auth") || !strings.Contains(text, "until a password is set") {
t.Errorf("%s no longer says the rebuilt store stays loopback-only until a password is set on it or reset-auth releases it, so choosing the rebuild reads as costing one start:\n%s", name, text)
}
for want, what := range map[string]string{
"switched on from the machine itself": "that network access switched on at the machine releases the hold too",
"and the daemon restarted or reloaded": "that a password set on the rebuilt store ends the hold once the daemon restarts or reloads",
"the copy lists": "that the recovered copy is checked for the install's settings before it replaces anything - a torn first page can leave nothing, and put in place that is a brand-new install with no login",
} {
if !strings.Contains(text, want) {
t.Errorf("%s no longer says %s (%q):\n%s", name, what, want, text)
}
}
}
// Where the flag says what the hold overrides, it has to say it overrides the
// flag: an operator choosing -on-corrupt rebuild in a unit that also passes
// -access network reads here whether the two can open the store together.
if !strings.Contains(flatDoc(help), "whatever -access says") {
t.Errorf("the curated help's -on-corrupt entry no longer says the rebuilt store stays loopback-only whatever -access says:\n%s", help)
}
if !strings.Contains(row, "even under `-access network`") {
t.Errorf("docs/cli.md's -on-corrupt row no longer says the rebuilt store stays loopback-only even under -access network:\n%s", row)
}
// The other places that say what ends the hold carry the same clause: a
// password set on the store counts only once the daemon restarts.
cli := mustReadRepoFile(t, filepath.Join("docs", "cli.md"))
var accessRow, resetAuthPara string
for _, u := range docUnits(cli) {
switch {
case strings.HasPrefix(u, "| `-access` |"):
accessRow = flatDoc(u)
case strings.Contains(flatDoc(u), "It also releases the hold"):
resetAuthPara = flatDoc(u)
}
}
accessNotes := readme
if k := strings.Index(accessNotes, "One store does not honour it"); k >= 0 {
accessNotes = accessNotes[k:]
if e := strings.Index(accessNotes, "The flip side"); e >= 0 {
accessNotes = accessNotes[:e]
}
} else {
accessNotes = ""
}
resetAuthNote := readme
if k := strings.Index(resetAuthNote, "also the way back onto the network for a container"); k >= 0 {
resetAuthNote = resetAuthNote[k:]
if e := strings.Index(resetAuthNote, "so claim it straight away"); e >= 0 {
resetAuthNote = resetAuthNote[:e]
}
} else {
resetAuthNote = ""
}
containerNote := readme
if k := strings.Index(containerNote, "bar a store rebuilt from a damaged database on request"); k >= 0 {
containerNote = containerNote[k:]
if e := strings.Index(containerNote, "It does not write the choice"); e >= 0 {
containerNote = containerNote[:e]
}
} else {
containerNote = ""
}
for name, text := range map[string]string{
"docs/cli.md's -access row": accessRow,
"docs/cli.md's reset-auth paragraph": resetAuthPara,
"README.md's -access notes": accessNotes,
"README.md's container upgrade note": containerNote,
"README.md's reset-auth paragraph": resetAuthNote,
} {
if !strings.Contains(text, "and the daemon restarted or reloaded") && !strings.Contains(text, "and the daemon is restarted or reloaded") {
t.Errorf("%s no longer says a password set on a rebuilt store ends the hold once the daemon restarts or reloads:\n%s", name, text)
}
}
// The refusal's own entry in the README: what to do with what .recover
// brought back, before it goes anywhere near the database's path.
i = strings.Index(readme, "- A database that won't open is left where it is.")
j := strings.Index(readme, "`-on-corrupt rebuild` takes the other road")
if i < 0 || j < i {
t.Fatal("README.md's corruption notes no longer open with the refusal entry ahead of the -on-corrupt rebuild entry")
}
for want, what := range map[string]string{
"SELECT key FROM settings": "the command that checks the recovered copy kept the install's settings",
"do not put that one back": "that a copy with no settings in it must not replace the damaged file",
} {
if !strings.Contains(readme[i:j], want) {
t.Errorf("README.md's refusal entry no longer says %s (%q):\n%s", what, want, readme[i:j])
}
}
}
// docUnits splits a markdown document into the pieces a reader takes in one at
// a time: paragraphs, except that a table is read row by row.
func docUnits(doc string) []string {
var units []string
for _, para := range strings.Split(strings.ReplaceAll(doc, "\r\n", "\n"), "\n\n") {
if strings.HasPrefix(strings.TrimSpace(para), "|") {
units = append(units, strings.Split(para, "\n")...)
continue
}
units = append(units, para)
}
return units
}
// flatDoc reads markdown the way a sentence check needs it: wrapping collapsed,
// blockquote markers and bold dropped, so a phrase that breaks across lines or
// wears emphasis is still the phrase.
func flatDoc(md string) string {
var words []string
for _, w := range strings.Fields(md) {
if w != ">" {
words = append(words, w)
}
}
return strings.ReplaceAll(strings.Join(words, " "), "**", "")
}
// In a container, a store with no birth marker and no stored access choice gets
// the ambiguous-provenance warning, whose way out is to restart with -access
// network. A store the daemon rebuilt after finding the database damaged has
// exactly that shape and none of the ambiguity - the daemon made it - and that
// way out is the one thing it does not honour. Its own warning says why the
// port answers 403; this one would send the operator round in a circle.
func TestARebuiltContainerStoreIsNotToldToRestartWithTheFlag(t *testing.T) {
ctx := context.Background()
dbPath := filepath.Join(t.TempDir(), "pingularity.db")
st, err := store.Open(dbPath)
if err != nil {
t.Fatalf("open: %v", err)
}
if err := st.SetSetting(ctx, "thresh_down_mbps", "123.5"); err != nil {
t.Fatal(err)
}
st.Close()
tearTableRoot(t, dbPath, "pauses")
st, err = store.Open(dbPath, store.RebuildOnCorruption())
if err != nil {
t.Fatalf("rebuild: %v", err)
}
defer st.Close()
holdStands, err := st.AccessHoldAfterRebuild(ctx)
if err != nil || !holdStands {
t.Fatalf("fixture: the rebuilt store holds nothing (held=%v, err=%v)", holdStands, err)
}
for _, flags := range []config.Config{
{DBPath: dbPath, Access: "network", AccessExplicit: true},
{DBPath: dbPath},
} {
cfg, _ := holdAccessLocalAfterRebuild(flags, holdStands)
set, err := settings.New(ctx, st, testDefaultsFor(cfg), settings.WithDatabaseCreated(false))
if err != nil {
t.Fatalf("settings: %v", err)
}
if warns := bootAccessDecision(t, cfg, st, set, true); len(warns) != 0 {
t.Errorf("a rebuilt container store (-access %q) was told its access provenance is ambiguous and to restart with -access network, which that store does not honour: %s", flags.Access, warns[0].msg)
}
}
// With the hold released the store is an ordinary unmarked one again, and the
// warning is the ordinary answer for it.
if _, err := st.ReleaseAccessHold(ctx); err != nil {
t.Fatal(err)
}
cfg := config.Config{DBPath: dbPath}
set, err := settings.New(ctx, st, testDefaultsFor(cfg), settings.WithDatabaseCreated(false))
if err != nil {
t.Fatalf("settings: %v", err)
}
if warns := bootAccessDecision(t, cfg, st, set, true); len(warns) != 1 {
t.Fatalf("fixture: with the hold released the unmarked container store got %d warnings, want the ordinary one - so this test proves nothing about the hold", len(warns))
}
}
// rebuiltDatabase leaves, at a fresh path, the database -on-corrupt rebuild made
// of an install that had a login and network access - closed, the way the next
// start finds it.
func rebuiltDatabase(t *testing.T) string {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "pingularity.db")
st, err := store.Open(dbPath)
if err != nil {
t.Fatalf("open: %v", err)
}
if err := st.SetSettings(context.Background(), map[string]string{
"auth_enabled": "1",
"auth_user": "admin",
"auth_hash": authHash,
"access_local_only": "0",
}); err != nil {
t.Fatalf("seed the install: %v", err)
}
st.Close()
tearTableRoot(t, dbPath, "pauses")
st, err = store.Open(dbPath, store.RebuildOnCorruption())
if err != nil {
t.Fatalf("rebuild: %v", err)
}
if !st.RebuiltAfterCorruption() {
st.Close()
t.Fatal("fixture: nothing was rebuilt")
}
st.Close()
return dbPath
}
// The TCP peers the guard judges: a machine on the LAN, and this one.
const (
lanPeer = "192.168.4.30:51515"
loopbackPeer = "127.0.0.1:51515"
)
// asPeer makes one request through srv's handler from peer and returns the
// status and the trimmed body; a body makes it a JSON POST, the only kind the
// API takes.
func asPeer(t *testing.T, srv *web.Server, peer, method, path, body string) (int, string) {
t.Helper()
r := httptest.NewRequest(method, path, strings.NewReader(body))
if body != "" {
r.Header.Set("Content-Type", "application/json")
}
r.RemoteAddr = peer
r.Host = "127.0.0.1:9000"
if peer == lanPeer {
r.Host = "192.168.4.39:9000"
}
w := httptest.NewRecorder()
srv.Handler().ServeHTTP(w, r)
return w.Code, strings.TrimSpace(w.Body.String())
}
// bootLikeRun is run()'s access sequence on st, in run()'s order and through
// run()'s own functions: the hold read that seeds the settings defaults, the
// settings load, the post-load access sequence, and the hook every later load
// runs. It returns the program - whose hold the web server reads - the
// controller, and every warning the boot raised.
func bootLikeRun(t *testing.T, flags config.Config, st *store.Store) (*program, *settings.Controller, []warnCall) {
t.Helper()
ctx := context.Background()
p := &program{cfg: flags, store: st, log: slog.New(slog.NewTextHandler(io.Discard, nil))}
var warns []warnCall
sink := func(msg string, args ...any) { warns = append(warns, warnCall{msg: msg, args: args}) }
set, err := settings.New(ctx, st, testDefaultsFor(p.accessSeedAtBoot(ctx, sink)))
if !settings.LoadedOK(err) {
t.Fatalf("settings: %v", err)
}
p.applyExplicitAccess(ctx, set, true, sink, sink)
p.registerSettingsLoadedHook(ctx, set)
return p, set, warns
}
// serverLikeRun is the web server run() builds over st, with the wire this file
// is about connected the way run() connects it: the guard and readiness read the
// program's hold.
func serverLikeRun(p *program, st *store.Store, set *settings.Controller) *web.Server {
srv := web.New(st, func() web.LiveStatus { return web.LiveStatus{Online: true} }, nil, set, nil, "test",
slog.New(slog.NewTextHandler(io.Discard, nil)))
srv.AccessHold = p.currentAccessHold
return srv
}
// bootWithUnreadableSettings is run()'s boot on a store whose settings - and so
// whether it carries the hold - cannot be read at that moment: dead fails both
// reads the way a transient fault does, leaving the unloaded controller run()
// carries into its retry loop, with the hook every later load runs registered.
// It returns the program, the controller, the server run() builds, and a reader
// for everything logged, early warnings included.
func bootWithUnreadableSettings(t *testing.T, flags config.Config, st *store.Store, dead context.Context) (*program, *settings.Controller, *web.Server, func() string) {
t.Helper()
logged := &lockedLog{}
p := &program{cfg: flags, store: st, log: slog.New(slog.NewTextHandler(logged, nil))}
seed := p.accessSeedAtBoot(dead, func(msg string, args ...any) { p.log.Warn(msg, args...) })
set, err := settings.New(dead, st, testDefaultsFor(seed))
if err == nil {
t.Fatal("fixture: the settings read at boot must fail")
}
if got := p.currentAccessHold(); got != web.AccessHeldUnread {
t.Fatalf("a boot that could not read the hold records it as %d, want unread: the network is decided on a read that failed", got)
}
p.registerSettingsLoadedHook(context.Background(), set)
return p, set, serverLikeRun(p, st, set), logged.String
}
// lockedLog is a log sink safe to read while a handler may still be writing.
type lockedLog struct {
mu sync.Mutex
b bytes.Buffer
}
func (l *lockedLog) Write(p []byte) (int, error) {
l.mu.Lock()
defer l.mu.Unlock()
return l.b.Write(p)
}
func (l *lockedLog) String() string {
l.mu.Lock()
defer l.mu.Unlock()
return l.b.String()
}