package main import ( "os" "path/filepath" "strings" "testing" ) // TestEmbeddedUnits guards the embedded systemd units against drift — the // instance template must carry the EnvironmentFile add-instance relies on. func TestEmbeddedUnits(t *testing.T) { if !strings.Contains(unitRouter, "ExecStart=/opt/lchat/lchat router") { t.Error("router unit missing its ExecStart line") } if !strings.Contains(unitInstance, "EnvironmentFile=-/data/%i.env") { t.Error("instance unit missing EnvironmentFile directive") } // Execs the per-instance live symlink (versioned deploys), not one shared binary. if !strings.Contains(unitInstance, "ExecStart=/opt/lchat/live/%i --socket /run/lchat/%i.sock --db /data/%i.db") { t.Error("instance unit missing its versioned ExecStart line") } // The bot execs the SAME per-instance live symlink as its instance, so its // binary tracks the app version (deployHostVersion restarts it on a move). if !strings.Contains(unitBot, "ExecStart=/opt/lchat/live/%i bot run") { t.Error("bot unit missing its versioned `bot run` ExecStart line") } if !strings.Contains(unitBot, "EnvironmentFile=-/data/%i-bot.env") { t.Error("bot unit missing its EnvironmentFile") } } // TestEmbeddedCollectors guards that the status collectors ride the binary: the // scripts + their units are embedded non-empty and match their source (the box // can no longer scp them in — the whole point of this install path). func TestEmbeddedCollectors(t *testing.T) { // A sentinel line from each source script — proves we embedded the real file, // not an empty/placeholder string, and pins the /opt/lchat ExecStart path the // units depend on. if !strings.Contains(scriptSSHLedger, "Installed on the box at exactly /opt/lchat/ssh-ledger-collect.sh") { t.Error("scriptSSHLedger missing its source sentinel line") } if !strings.Contains(scriptHardening, "Public fleet hardening posture — the box-side collector") { t.Error("scriptHardening missing its source sentinel line") } // Both scripts are bash — the shebang must survive the embed so systemd can // exec them. for name, s := range map[string]string{"ssh-ledger": scriptSSHLedger, "hardening": scriptHardening} { if !strings.HasPrefix(s, "#!/usr/bin/env bash") { t.Errorf("%s script missing its shebang", name) } } // The four units + their ExecStart / timer markers. for name, u := range map[string]string{ "ssh-ledger.service": unitSSHLedger, "ssh-ledger.timer": unitSSHLedgerTimer, "hardening.service": unitHardening, "hardening.timer": unitHardeningTimer, } { if strings.TrimSpace(u) == "" { t.Errorf("unit %s embedded empty", name) } } if !strings.Contains(unitSSHLedger, "ExecStart=/opt/lchat/ssh-ledger-collect.sh") { t.Error("ssh-ledger unit missing its ExecStart line") } if !strings.Contains(unitHardening, "ExecStart=/opt/lchat/hardening-posture.sh") { t.Error("hardening unit missing its ExecStart line") } } // TestWriteCollectorFiles drives the shared install helper into temp dirs (no // systemctl, no /opt/lchat) and asserts every file lands at the right path with // the right mode: scripts executable (0755), units + pages 0644, and the viewer // pages written basename-only into /status-pages. func TestWriteCollectorFiles(t *testing.T) { appDir := t.TempDir() unitDir := t.TempDir() dataDir := t.TempDir() writeCollectorFiles(appDir, unitDir, dataDir) // The collectors' state dirs must exist before their units first start: a // missing ReadWritePaths target fails the unit's namespace setup — even its // `+`-prefixed ExecStartPre mkdir (the first-real-run failure, 2026-07-19). for _, d := range []string{"ssh-ledger", "hardening"} { info, err := os.Stat(filepath.Join(dataDir, d)) if err != nil || !info.IsDir() { t.Errorf("collector state dir %s not created: %v", d, err) } } // Scripts: present, executable, and byte-identical to the embedded source. for _, tc := range []struct { name string want string }{ {"ssh-ledger-collect.sh", scriptSSHLedger}, {"hardening-posture.sh", scriptHardening}, } { p := filepath.Join(appDir, tc.name) assertFileMode(t, p, 0o755) got, err := os.ReadFile(p) if err != nil { t.Fatalf("read %s: %v", tc.name, err) } if string(got) != tc.want { t.Errorf("%s content differs from embedded source", tc.name) } } // Units: present at 0644 in the unit dir. for _, u := range []string{ "lchat-ssh-ledger.service", "lchat-ssh-ledger.timer", "lchat-hardening.service", "lchat-hardening.timer", } { assertFileMode(t, filepath.Join(unitDir, u), 0o644) } // Pages: every embedded .html + status.css, basename-only, at 0644 — and // nothing else (no ssh/ dir, no JSON). pagesDir := filepath.Join(appDir, "status-pages") for _, page := range []string{ "index.html", "hardening.html", "ssh.html", "music.html", "status.css", "status-nav.js", } { assertFileMode(t, filepath.Join(pagesDir, page), 0o644) } for _, unexpected := range []string{"carbon.json", "ssh-operators.json", "icon-192.png"} { if _, err := os.Stat(filepath.Join(pagesDir, unexpected)); err == nil { t.Errorf("status-pages should not contain generated/non-embedded %s", unexpected) } } } // TestEnableCollectorTimers checks the helper enables + fires both timers. func TestEnableCollectorTimers(t *testing.T) { oldSysctl := systemctl t.Cleanup(func() { systemctl = oldSysctl }) var calls []string systemctl = func(args ...string) error { calls = append(calls, strings.Join(args, " ")) return nil } enableCollectorTimers() want := []string{ "enable --now lchat-ssh-ledger.timer", "enable --now lchat-hardening.timer", } if len(calls) != len(want) { t.Fatalf("systemctl calls = %v, want %v", calls, want) } for i, w := range want { if calls[i] != w { t.Errorf("call %d = %q, want %q", i, calls[i], w) } } } // TestSyncLinks: the redirector runs on the apex box and nowhere else. // enable+restart (never `enable --now`) so the installer's update path // re-execs the new binary rather than leaving the old process up; disable // --now on every other box so a fleet doesn't carry idle redirectors. func TestSyncLinks(t *testing.T) { for _, tc := range []struct { name string apex bool want []string }{ {"apex box runs it", true, []string{"enable lchat-links", "restart lchat-links"}}, {"every other box does not", false, []string{"disable --now lchat-links"}}, } { t.Run(tc.name, func(t *testing.T) { oldSysctl := systemctl t.Cleanup(func() { systemctl = oldSysctl }) var calls []string systemctl = func(args ...string) error { calls = append(calls, strings.Join(args, " ")) return nil } syncLinks(tc.apex) if len(calls) != len(tc.want) { t.Fatalf("systemctl calls = %v, want %v", calls, tc.want) } for i, w := range tc.want { if calls[i] != w { t.Errorf("call %d = %q, want %q", i, calls[i], w) } } }) } } // TestSyncLinksSurvivesFailure: short links are one optional service, so a box // that can't drive the unit must still finish bootstrapping rather than die(). func TestSyncLinksSurvivesFailure(t *testing.T) { oldSysctl := systemctl t.Cleanup(func() { systemctl = oldSysctl }) systemctl = func(args ...string) error { return os.ErrPermission } syncLinks(true) // must return normally rather than exiting syncLinks(false) // ditto for the disable path } // TestLinksRouted: the registry is what decides whether this box is the apex, // and it is matched on the SOCKET, not a hostname — that is what lets the same // code recognise 11ch.at in production and dev.11ch.at on the dev mesh without // either being named here. // // It is also what makes re-running bootstrap safe: the apex box keeps its // route, so a later plain `bootstrap` sees routed and leaves short links up, // instead of silently switching them off the way a flag-keyed design would. func TestLinksRouted(t *testing.T) { newRegistry := func(t *testing.T, routes ...Route) string { t.Helper() path := filepath.Join(t.TempDir(), "registry.db") reg, err := openRegistry(path) if err != nil { t.Fatalf("open registry: %v", err) } for _, rt := range routes { if err := reg.upsert(rt); err != nil { t.Fatalf("seed %s: %v", rt.Host, err) } } return path } t.Run("no routes at all", func(t *testing.T) { if linksRouted(newRegistry(t)) { t.Error("an empty registry must not read as the apex") } }) t.Run("instances only", func(t *testing.T) { path := newRegistry(t, Route{Host: "a.example.com", Kind: "instance", Socket: "/run/lchat/a.example.com.sock", Status: "ready"}, Route{Host: "home.example.com", Kind: "service", Socket: "/run/lchat/home.sock", Status: "ready"}, ) if linksRouted(path) { t.Error("a box with instances and other services must not read as the apex") } }) t.Run("production apex", func(t *testing.T) { path := newRegistry(t, Route{Host: "a.example.com", Kind: "instance", Socket: "/run/lchat/a.example.com.sock", Status: "ready"}, Route{Host: "11ch.at", Kind: "service", Socket: linksSocket, Status: "ready"}, ) if !linksRouted(path) { t.Error("a route pointing at the links socket means this box IS the apex") } }) t.Run("dev apex — recognised without being named", func(t *testing.T) { path := newRegistry(t, Route{Host: "dev.11ch.at", Kind: "service", Socket: linksSocket, Status: "ready"}) if !linksRouted(path) { t.Error("the apex is identified by socket, so any hostname must work") } }) t.Run("unreadable registry is not the apex", func(t *testing.T) { // Fail safe: an idle box beats starting a service we couldn't justify. if linksRouted(filepath.Join(t.TempDir(), "nonexistent", "registry.db")) { t.Error("an unreadable registry must not read as the apex") } }) // The fresh-box case, and the one that actually guards a footgun: /data // exists but the registry does not yet. openRegistry CREATES the database, // and this read runs as root on every plain bootstrap (including every // self-hoster's install.sh) — while the router, running as the lchat user, // creates the registry itself moments earlier. If this read won that race // it would leave a root-owned registry the router cannot write, breaking // all routing on a fresh box. So it must answer "not the apex" WITHOUT // creating anything. t.Run("absent registry is not the apex and is not created", func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "registry.db") if linksRouted(path) { t.Error("an absent registry must not read as the apex") } entries, err := os.ReadDir(dir) if err != nil { t.Fatalf("read dir: %v", err) } for _, e := range entries { t.Errorf("linksRouted created %s — a read must never create the registry", e.Name()) } }) } // TestRestartControlUnitsIncludesLinks: the units that exec /opt/lchat/lchat // directly (rather than a versioned live symlink) keep their OLD inode until // something restarts them, so every one of them must ride the updater's // restart pass — otherwise the redirector is the one service that never picks // up a new build. func TestRestartControlUnitsIncludesLinks(t *testing.T) { oldSysctl := systemctl t.Cleanup(func() { systemctl = oldSysctl }) var calls []string systemctl = func(args ...string) error { calls = append(calls, strings.Join(args, " ")) return nil } restartControlUnits() want := []string{ "try-restart lchat-control.service", "try-restart lchat-cloud.service", "try-restart lchat-links.service", } if len(calls) != len(want) { t.Fatalf("systemctl calls = %v, want %v", calls, want) } for i, w := range want { if calls[i] != w { t.Errorf("call %d = %q, want %q", i, calls[i], w) } } } // TestSetUpdateTimer: the off path must DISABLE, not merely skip enabling. A // box bootstrapped normally once already has the timer on, and on the dev mesh // an updater pass would replace unmerged code with the current stable build — // so the end state has to follow the flag, not the box's history. func TestSetUpdateTimer(t *testing.T) { for _, tc := range []struct { name string on bool want string }{ {"on enables", true, "enable --now lchat-update.timer"}, {"off disables", false, "disable --now lchat-update.timer"}, } { t.Run(tc.name, func(t *testing.T) { oldSysctl := systemctl t.Cleanup(func() { systemctl = oldSysctl }) var calls []string systemctl = func(args ...string) error { calls = append(calls, strings.Join(args, " ")) return nil } setUpdateTimer(tc.on) if len(calls) != 1 || calls[0] != tc.want { t.Errorf("systemctl calls = %v, want [%q]", calls, tc.want) } }) } } // TestLinksUnitShape guards the embedded unit against drift: it must exec the // links subcommand on the socket bootstrap's route registration expects, and // must NOT carry an EnvironmentFile (it holds no secrets — the reason enabling // it everywhere is safe). func TestLinksUnitShape(t *testing.T) { if !strings.Contains(unitLinks, "ExecStart=/opt/lchat/lchat links run --socket /run/lchat/links.sock") { t.Error("links unit missing its ExecStart line (socket path is the route contract)") } if strings.Contains(unitLinks, "EnvironmentFile") { t.Error("links unit should carry no EnvironmentFile — it holds no secrets") } } // assertFileMode asserts a file exists with exactly the given permission bits. func assertFileMode(t *testing.T, path string, want os.FileMode) { t.Helper() fi, err := os.Stat(path) if err != nil { t.Fatalf("stat %s: %v", path, err) } if got := fi.Mode().Perm(); got != want { t.Errorf("%s mode = %o, want %o", path, got, want) } } // TestOperatorsExist pins the lockdown gate: only a group file whose // lchat-operators entry actually lists members opens it — a missing file, // missing group, or empty member list must all read as "no operators". func TestOperatorsExist(t *testing.T) { cases := map[string]struct { content string want bool }{ "members": {"root:x:0:\nlchat-operators:x:990:paul,james\n", true}, "one member": {"lchat-operators:x:990:paul\n", true}, "empty list": {"lchat-operators:x:990:\n", false}, "blank list": {"lchat-operators:x:990: \n", false}, "no group": {"root:x:0:\nlchat:x:999:\n", false}, "name is prefix only": {"lchat-operators-old:x:991:paul\n", false}, } for name, tc := range cases { p := filepath.Join(t.TempDir(), "group") if err := os.WriteFile(p, []byte(tc.content), 0o644); err != nil { t.Fatal(err) } if got := operatorsExist(p); got != tc.want { t.Errorf("%s: operatorsExist = %v, want %v", name, got, tc.want) } } if operatorsExist(filepath.Join(t.TempDir(), "missing")) { t.Error("missing group file should read as no operators") } } // TestNoRootDropIn guards the drop-in contract: the directive itself, and the // filename ordering that makes it effective — sshd config is first-match-wins, // so the file must sort before the distro's 50-*.conf (prohibit-password). func TestNoRootDropIn(t *testing.T) { if !strings.Contains(noRootConf, "PermitRootLogin no\n") { t.Error("drop-in missing its PermitRootLogin no directive") } base := filepath.Base(noRootConfPath) if base >= "50-" { t.Errorf("drop-in %s sorts after 50-*.conf — it would lose first-match to the distro default", base) } if filepath.Dir(noRootConfPath) != "/etc/ssh/sshd_config.d" { t.Errorf("drop-in in %s, want /etc/ssh/sshd_config.d", filepath.Dir(noRootConfPath)) } } // TestApplyNoRootSSH covers the converge flow with the exec seams stubbed: // fresh write reloads sshd, an already-correct file is a silent no-op, a // failed validation restores exactly the prior state (file absent or prior // content) so sshd's next real restart can never hit config it would refuse, // and a drop-in that fails the EFFECTIVE-config check errors on both the // fresh and idempotent paths (the kepler 00-hardening.conf shape). func TestApplyNoRootSSH(t *testing.T) { oldCheck, oldSysctl, oldEff := sshdCheck, systemctl, sshdRootLogin defer func() { sshdCheck, systemctl, sshdRootLogin = oldCheck, oldSysctl, oldEff }() var reloads []string systemctl = func(args ...string) error { reloads = append(reloads, strings.Join(args, " ")) return nil } sshdCheck = func() error { return nil } sshdRootLogin = func() (string, error) { return "no", nil } p := filepath.Join(t.TempDir(), "10-lchat-noroot.conf") changed, err := applyNoRootSSH(p) if err != nil || !changed { t.Fatalf("fresh apply: changed=%v err=%v, want true, nil", changed, err) } if b, _ := os.ReadFile(p); string(b) != noRootConf { t.Errorf("drop-in content = %q", b) } if len(reloads) != 1 || reloads[0] != "reload ssh" { t.Errorf("reloads = %v, want [reload ssh]", reloads) } // Second pass: already converged — no write, no reload. reloads = nil if changed, err = applyNoRootSSH(p); err != nil || changed { t.Fatalf("idempotent apply: changed=%v err=%v, want false, nil", changed, err) } if len(reloads) != 0 { t.Errorf("idempotent apply reloaded sshd: %v", reloads) } // Validation failure over an existing (stale) file restores it verbatim. stale := "PermitRootLogin prohibit-password\n" if err := os.WriteFile(p, []byte(stale), 0o644); err != nil { t.Fatal(err) } sshdCheck = func() error { return os.ErrInvalid } if _, err = applyNoRootSSH(p); err == nil { t.Fatal("apply with failing sshd -t should error") } if b, _ := os.ReadFile(p); string(b) != stale { t.Errorf("failed validation left %q, want prior content restored", b) } // Validation failure on a fresh box removes the file entirely. p2 := filepath.Join(t.TempDir(), "10-lchat-noroot.conf") if _, err = applyNoRootSSH(p2); err == nil { t.Fatal("apply with failing sshd -t should error") } if _, statErr := os.Stat(p2); !os.IsNotExist(statErr) { t.Errorf("failed validation left the drop-in behind (stat err %v)", statErr) } // An earlier-sorting file overriding the drop-in (effective config stays // prohibit-password): the fresh write must error — success here is the // kepler bug — and the drop-in stays in place (inert, not harmful) for the // re-run after the operator fixes the ordering. sshdCheck = func() error { return nil } sshdRootLogin = func() (string, error) { return "prohibit-password", nil } p3 := filepath.Join(t.TempDir(), "10-lchat-noroot.conf") if _, err = applyNoRootSSH(p3); err == nil || !strings.Contains(err.Error(), "first-match-wins") { t.Fatalf("ineffective drop-in: err = %v, want first-match-wins explanation", err) } if b, _ := os.ReadFile(p3); string(b) != noRootConf { t.Errorf("ineffective drop-in should stay in place, got %q", b) } // Same on the idempotent path: an already-correct file that isn't winning // must not report success. if _, err = applyNoRootSSH(p3); err == nil { t.Fatal("idempotent apply over an ineffective drop-in should error") } } func TestCountAuthorizedKeys(t *testing.T) { cases := []struct { name string in string n int ok bool }{ {"one ed25519", "ssh-ed25519 AAAAC3Nza james@mac\n", 1, true}, {"two with comments and blanks", "# ci key\nssh-ed25519 AAAA a\n\nsk-ssh-ed25519@openssh.com BBBB b\n", 2, true}, {"ecdsa", "ecdsa-sha2-nistp256 AAAA\n", 1, true}, {"empty", "", 0, false}, {"whitespace only", "\n \n", 0, false}, {"junk line", "ssh-ed25519 AAAA ok\nnot a key\n", 0, false}, {"missing key body", "ssh-ed25519\n", 0, false}, } for _, c := range cases { n, err := countAuthorizedKeys([]byte(c.in)) if (err == nil) != c.ok || n != c.n { t.Errorf("%s: n=%d err=%v, want n=%d ok=%v", c.name, n, err, c.n, c.ok) } } } // TestAddOperatorAccount drives the whole verb through the exec/user seams: // the create path must run useradd, the converge path must not, and a bad // keys pipe must fail BEFORE any account command runs — with root SSH off, a // clobbered authorized_keys is a console/rescue trip. func TestAddOperatorAccount(t *testing.T) { oldRun, oldLookup := runAccountCmd, lookupAccount defer func() { runAccountCmd, lookupAccount = oldRun, oldLookup }() home := t.TempDir() exists := false var calls []string runAccountCmd = func(name string, args ...string) error { calls = append(calls, name+" "+strings.Join(args, " ")) if name == "useradd" { exists = true } return nil } lookupAccount = func(string) (string, int, int, bool) { return home, os.Getuid(), os.Getgid(), exists } keys := "ssh-ed25519 AAAA james@mac\nssh-ed25519 BBBB james@laptop\n" if err := addOperatorAccount("james", strings.NewReader(keys)); err != nil { t.Fatalf("create: %v", err) } want := []string{"groupadd -f lchat-operators", "useradd -m -s /bin/bash james", "usermod -aG lchat-operators,systemd-journal james"} if strings.Join(calls, ";") != strings.Join(want, ";") { t.Errorf("create calls = %v, want %v", calls, want) } ak := filepath.Join(home, ".ssh", "authorized_keys") if b, err := os.ReadFile(ak); err != nil || string(b) != keys { t.Errorf("authorized_keys = %q, %v", b, err) } for p, want := range map[string]os.FileMode{filepath.Join(home, ".ssh"): 0o700, ak: 0o600} { if fi, err := os.Stat(p); err != nil || fi.Mode().Perm() != want { t.Errorf("%s mode = %v, want %v", p, fi.Mode().Perm(), want) } } // Converge an existing account with a rotated key set: no useradd, and // the file is REPLACED, not appended. calls = nil rotated := "ssh-ed25519 CCCC james@new\n" if err := addOperatorAccount("james", strings.NewReader(rotated)); err != nil { t.Fatalf("converge: %v", err) } for _, c := range calls { if strings.HasPrefix(c, "useradd") { t.Errorf("converge ran useradd: %v", calls) } } if b, _ := os.ReadFile(ak); string(b) != rotated { t.Errorf("rotated authorized_keys = %q, want replacement", b) } // Bad pipes and bad names fail before any account command. calls = nil if err := addOperatorAccount("james", strings.NewReader("")); err == nil { t.Error("empty stdin should error") } if err := addOperatorAccount("James Adam", strings.NewReader(rotated)); err == nil { t.Error("invalid account name should error") } if len(calls) != 0 { t.Errorf("failed runs still executed commands: %v", calls) } }