123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 | package setup_test
import (
"archive/tar"
"bytes"
"errors"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"slices"
"strings"
"testing"
"testing/fstest"
"code.bruce-hill.com/repo-renderer/internal/config"
"code.bruce-hill.com/repo-renderer/internal/setup"
)
// fakeOS is a system under a temporary root, with the commands the
// checks run and httpd's answers standing in for the real ones.
type fakeOS struct {
t *testing.T
root string
owners map[string]string // path → "owner:group"; missing is root:wheel
// httpd: whether it is running, whether httpd -n rejects its
// configuration, and what status it answers the site's page with.
running, rejects bool
status int
checkFails bool // repo-render check fails
env map[string]string
}
var share = fstest.MapFS{
"hooks/post-receive": {Data: []byte("#!/bin/sh\nexec repo-render enqueue\n")},
"lexers/tomo.xml": {Data: []byte("<lexer/>\n")},
"examples/repo-render.yaml": {Data: []byte("host: code.example.com\n")},
}
func (f *fakeOS) path(p string) string { return filepath.Join(f.root, p) }
func (f *fakeOS) write(p, data string, mode fs.FileMode, owner string) {
f.t.Helper()
if err := os.MkdirAll(filepath.Dir(f.path(p)), 0o755); err != nil {
f.t.Fatal(err)
}
if err := os.WriteFile(f.path(p), []byte(data), mode); err != nil {
f.t.Fatal(err)
}
if err := os.Chmod(f.path(p), mode); err != nil {
f.t.Fatal(err)
}
if owner != "" {
f.owners[p] = owner
}
}
func (f *fakeOS) mkdir(p string, mode fs.FileMode, owner string) {
f.t.Helper()
if err := os.MkdirAll(f.path(p), 0o755); err != nil {
f.t.Fatal(err)
}
if err := os.Chmod(f.path(p), mode); err != nil {
f.t.Fatal(err)
}
if owner != "" {
f.owners[p] = owner
}
}
func (f *fakeOS) run(e *setup.Env, c setup.Cmd) ([]byte, error) {
switch {
case c.Argv[0] == "httpd" && f.rejects:
return []byte("/etc/httpd.conf:3: syntax error"), errors.New("httpd -n failed")
case c.Argv[0] == "rcctl" && !f.running:
return []byte("httpd(failed)"), errors.New("not running")
case c.Argv[0] == setup.BinPath && f.checkFails:
return []byte("lexer \"tomo\" resolves to plain text"), errors.New("check failed")
}
return nil, nil
}
func (f *fakeOS) fetch(url, host string) (*http.Response, error) {
if url != "http://127.0.0.1:8080/" || host != "code.example.com" {
f.t.Errorf("fetched %s for %s", url, host)
}
h := http.Header{}
if f.status == 200 {
h.Set("Content-Security-Policy", "default-src 'none'")
}
return &http.Response{StatusCode: f.status, Status: http.StatusText(f.status), Header: h, Body: io.NopCloser(strings.NewReader(""))}, nil
}
// fresh is an OpenBSD install with nothing of repo-render's on it.
func fresh(t *testing.T) (*fakeOS, *setup.Env) {
f := &fakeOS{t: t, root: t.TempDir(), owners: map[string]string{}, status: 200, env: map[string]string{}}
f.write("/etc/passwd", "root:*:0:0::/root:/bin/ksh\nbruce:*:1000:1000::/home/bruce:/bin/ksh\n", 0o644, "")
f.write("/etc/group", "wheel:*:0:root\nbin:*:7:\n", 0o644, "")
f.write("/etc/shells", "/bin/sh\n/bin/ksh\n", 0o644, "")
f.write("/etc/installurl", "https://cdn.openbsd.org/pub/OpenBSD\n", 0o644, "")
f.write("/root/.ssh/authorized_keys", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA root@laptop\n", 0o600, "")
for _, d := range []string{"/var/www/htdocs", "/var/db", "/var/spool", "/usr/local/bin", "/usr/local/share"} {
f.mkdir(d, 0o755, "")
}
e := &setup.Env{Root: f.root, Run: f.run, Fetch: f.fetch, AsRoot: true,
Getenv: func(k string) string { return f.env[k] },
OwnerOf: func(p string) (string, string, error) {
if _, err := os.Lstat(f.path(p)); err != nil {
return "", "", err
}
o, ok := f.owners[p]
if !ok {
o = "root:wheel"
}
owner, group, _ := strings.Cut(o, ":")
return owner, group, nil
}}
return f, e
}
// ready is a system with everything in place.
func ready(t *testing.T) (*fakeOS, *setup.Env) {
f, e := fresh(t)
f.write("/etc/passwd", "root:*:0:0::/root:/bin/ksh\ngit:*:1001:1001::/var/git:/usr/local/bin/git-shell\n", 0o644, "")
f.write("/etc/group", "wheel:*:0:root\nbin:*:7:\ngit:*:1001:\n", 0o644, "")
f.write(setup.GitPath, "git", 0o755, "")
f.write(setup.GitShell, "git-shell", 0o755, "")
f.write("/etc/shells", "/bin/sh\n/bin/ksh\n/usr/local/bin/git-shell\n", 0o644, "")
f.write(config.DefaultPath, "host: code.example.com\n", 0o644, "")
f.mkdir("/var/git", 0o755, "git:git")
for _, d := range []string{"/var/www/htdocs/code.example.com", "/var/db/repo-render", "/var/spool/repo-render"} {
f.mkdir(d, 0o755, "git:git")
}
f.mkdir("/var/git/.ssh", 0o700, "git:git")
f.write("/var/git/.ssh/authorized_keys", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA root@laptop\n", 0o600, "git:git")
f.write(setup.BinPath, "repo-render", 0o755, "")
_ = fs.WalkDir(share, ".", func(name string, d fs.DirEntry, err error) error {
if d.IsDir() {
return err
}
dest, mode := filepath.Join(setup.ShareDir, name), fs.FileMode(0o644)
switch {
case strings.HasPrefix(name, "hooks/"):
mode = 0o755
case strings.HasPrefix(name, "examples/"):
dest = filepath.Join(setup.ShareDir, filepath.Base(name))
}
f.write(dest, string(share[name].Data), mode, "")
return nil
})
f.write(setup.HTTPDPath, "prefork 3\n"+setup.IncludeLine+"\n", 0o644, "")
cfg, err := config.Parse([]byte("host: code.example.com\n"))
if err != nil {
t.Fatal(err)
}
conf, err := setup.SiteConf(e, cfg)
if err != nil {
t.Fatal(err)
}
f.write(setup.IncludePath, string(conf), 0o644, "")
f.write(setup.RCConfLocal, "httpd_flags=\n", 0o644, "")
f.write("/var/www/htdocs/code.example.com/index.html", "<!doctype html>", 0o644, "git:git")
f.running = true
return f, e
}
func options(f *fakeOS) setup.Options {
return setup.Options{ConfigPath: config.DefaultPath, Self: f.path(setup.BinPath), Share: share}
}
// byTitle finds a check.
func byTitle(t *testing.T, checks []*setup.Check, title string) *setup.Check {
t.Helper()
for _, c := range checks {
if c.Title == title {
return c
}
}
t.Fatalf("no check %q", title)
return nil
}
// failing lists the checks that are not OK, with their fixes.
func failing(checks []*setup.Check) []string {
var out []string
for _, c := range checks {
if c.Status != setup.OK {
out = append(out, c.Title+": "+c.Detail+": "+strings.Join(c.Fix, "; "))
}
}
return out
}
func TestReadySystemPasses(t *testing.T) {
f, e := ready(t)
checks := setup.Run(e, options(f))
if left := failing(checks); len(left) > 0 {
t.Errorf("a ready system fails:\n%s", strings.Join(left, "\n"))
}
var out bytes.Buffer
if n := setup.Report(&out, checks, false); n != 0 {
t.Errorf("Report counts %d not done:\n%s", n, out.String())
}
}
func TestFreshSystem(t *testing.T) {
f, e := fresh(t)
f.env["DOAS_USER"] = "bruce" // who has no keys: root's are taken
f.write("/home/bruce/repo-render", "repo-render", 0o755, "")
o := options(f)
o.Host, o.Self = "code.example.com", f.path("/home/bruce/repo-render")
checks := setup.Run(e, o)
want := map[string][]string{
"git": {"pkg_add git"},
"git account": {"useradd -d /var/git -g =uid -s /usr/local/bin/git-shell git"},
"configuration": {"install -d -m 755 /etc/repo-render", "echo 'host: code.example.com' > /etc/repo-render/repo-render.yaml"},
"repositories directory": {"install -d -o git -g git -m 755 /var/git"},
"site directory": {"install -d -o git -m 755 /var/www/htdocs/code.example.com"},
"SSH keys that may push": {"install -d -o git -g git -m 700 /var/git/.ssh", "install -o git -g git -m 600 /dev/null /var/git/.ssh/authorized_keys", "cat /root/.ssh/authorized_keys >> /var/git/.ssh/authorized_keys"},
"repo-render": {"install -o root -g bin -m 755 " + o.Self + " " + setup.BinPath},
"httpd.conf includes the site": {"echo 'include \"/etc/repo-render/httpd.conf\"' >> /etc/httpd.conf"},
"httpd starts at boot": {"rcctl enable httpd"},
// It reads the config the configuration's fix writes.
"the site's httpd configuration": {o.Self + " httpd-conf > " + setup.IncludePath},
}
for title, fix := range want {
c := byTitle(t, checks, title)
if c.Status != setup.Fix || !slices.Equal(c.Fix, fix) {
t.Errorf("%s: %v %q, want ✗ %q (%s)", title, c.Status, c.Fix, fix, c.Detail)
}
}
for _, title := range []string{"git-shell is a login shell", "httpd accepts its configuration", "the site is rendered", "httpd serves the site"} {
if c := byTitle(t, checks, title); c.Status != setup.Later {
t.Errorf("%s is %v, want waiting on earlier checks", title, c.Status)
}
}
}
func TestNoHostGivenUsesPlaceholder(t *testing.T) {
f, e := fresh(t)
checks := setup.Run(e, options(f))
c := byTitle(t, checks, "configuration")
if !slices.Contains(c.Fix, "echo 'host: HOST' > /etc/repo-render/repo-render.yaml") || !strings.Contains(c.Detail, "--host") {
t.Errorf("config fix %q (%s)", c.Fix, c.Detail)
}
// With no host, the config written may not be the one in the end,
// and the site's directory has no name yet.
for _, title := range []string{"the site's httpd configuration", "site directory"} {
if c := byTitle(t, checks, title); c.Status != setup.Later {
t.Errorf("%s with no host yet is %v %q, want waiting", title, c.Status, c.Fix)
}
}
}
func TestSiteOwnedBySomeoneElse(t *testing.T) {
f, e := ready(t)
f.mkdir("/var/www/htdocs/code.example.com", 0o775, "bruce:www-data")
checks := setup.Run(e, options(f))
c := byTitle(t, checks, "site directory")
want := []string{"chown -R git /var/www/htdocs/code.example.com", "chmod 755 /var/www/htdocs/code.example.com"}
if c.Status != setup.Fix || !slices.Equal(c.Fix, want) {
t.Errorf("site directory: %v %q, want %q", c.Status, c.Fix, want)
}
if c := byTitle(t, checks, "httpd serves the site"); c.Status != setup.Later {
t.Errorf("serving is %v before the site directory is fixed", c.Status)
}
}
func TestGitHomeElsewhere(t *testing.T) {
f, e := ready(t)
f.write("/etc/passwd", "root:*:0:0::/root:/bin/ksh\ngit:*:1001:1001::/home/git:/bin/ksh\n", 0o644, "")
c := byTitle(t, setup.Run(e, options(f)), "git account")
want := []string{"chsh -s /usr/local/bin/git-shell git", "usermod -d /var/git git"}
if c.Status != setup.Fix || !slices.Equal(c.Fix, want) {
t.Errorf("git account: %v %q, want %q", c.Status, c.Fix, want)
}
}
func TestShellAddedWhenGitIsThere(t *testing.T) {
f, e := ready(t)
f.write("/etc/shells", "/bin/sh\n", 0o644, "")
c := byTitle(t, setup.Run(e, options(f)), "git-shell is a login shell")
if want := []string{"echo /usr/local/bin/git-shell >> /etc/shells"}; !slices.Equal(c.Fix, want) {
t.Errorf("shells fix %q, want %q", c.Fix, want)
}
}
func TestConfigMustBeRoots(t *testing.T) {
f, e := ready(t)
f.owners[config.DefaultPath] = "git:git"
c := byTitle(t, setup.Run(e, options(f)), "configuration")
if !slices.Contains(c.Fix, "chown root:wheel "+config.DefaultPath) {
t.Errorf("a config git owns gets %q", c.Fix)
}
}
func TestKeysGitDoesNotOwn(t *testing.T) {
f, e := ready(t)
f.owners["/var/git/.ssh/authorized_keys"] = "root:wheel"
c := byTitle(t, setup.Run(e, options(f)), "SSH keys that may push")
if c.Status != setup.Fix || !slices.Contains(c.Fix, "chown git:git /var/git/.ssh/authorized_keys") {
t.Errorf("keys: %v %q", c.Status, c.Fix)
}
}
func TestKeysUnreadableWithoutRoot(t *testing.T) {
if os.Geteuid() == 0 {
t.Skip("root reads it anyway")
}
f, e := ready(t)
if err := os.Chmod(f.path("/var/git/.ssh/authorized_keys"), 0); err != nil {
t.Fatal(err)
}
e.AsRoot = false
if c := byTitle(t, setup.Run(e, options(f)), "SSH keys that may push"); c.Status != setup.Unknown {
t.Errorf("unreadable keys are %v, want unknown", c.Status)
}
}
func TestStaleShareFiles(t *testing.T) {
f, e := ready(t)
f.write(filepath.Join(setup.ShareDir, "hooks/post-receive"), "old hook", 0o755, "")
o := options(f)
c := byTitle(t, setup.Run(e, o), "hook, lexers and examples")
want := []string{o.Self + " share-files | tar -xpf - -C " + setup.ShareDir}
if c.Status != setup.Fix || !slices.Equal(c.Fix, want) {
t.Errorf("share files: %v %q, want %q", c.Status, c.Fix, want)
}
}
func TestNewerProgramToInstall(t *testing.T) {
f, e := ready(t)
f.write("/root/repo-render", "repo-render, newer", 0o755, "")
o := options(f)
o.Self = f.path("/root/repo-render")
c := byTitle(t, setup.Run(e, o), "repo-render")
if want := []string{"install -o root -g bin -m 755 " + o.Self + " " + setup.BinPath}; c.Status != setup.Fix || !slices.Equal(c.Fix, want) {
t.Errorf("program: %v %q, want %q", c.Status, c.Fix, want)
}
}
func TestSiteConfOutOfDate(t *testing.T) {
f, e := ready(t)
f.write(setup.IncludePath, "server \"code.example.com\" {}\n", 0o644, "")
o := options(f)
c := byTitle(t, setup.Run(e, o), "the site's httpd configuration")
if want := []string{o.Self + " httpd-conf > " + setup.IncludePath}; c.Status != setup.Fix || !slices.Equal(c.Fix, want) {
t.Errorf("site conf: %v %q, want %q", c.Status, c.Fix, want)
}
}
func TestOtherServerForHost(t *testing.T) {
f, e := ready(t)
f.write(setup.HTTPDPath, "server \"code.example.com\" {\n\tlisten on * port 80\n}\n"+setup.IncludeLine+"\n", 0o644, "")
c := byTitle(t, setup.Run(e, options(f)), "httpd.conf includes the site")
if c.Status != setup.Fix || len(c.Fix) > 0 || !strings.Contains(c.Detail, "take it out") {
t.Errorf("another server for the host: %v %q %s", c.Status, c.Fix, c.Detail)
}
}
func TestTypesOnlyOnce(t *testing.T) {
f, e := ready(t)
cfg, _ := config.Parse([]byte("host: code.example.com\n"))
b, _ := setup.SiteConf(e, cfg)
if !bytes.Contains(b, []byte("types {")) {
t.Error("with no types in httpd.conf, the site's has none")
}
f.write(setup.HTTPDPath, "types {\n\tinclude \"/usr/share/misc/mime.types\"\n}\n"+setup.IncludeLine+"\n", 0o644, "")
b, _ = setup.SiteConf(e, cfg)
if bytes.Contains(b, []byte("types {")) {
t.Error("httpd.conf has types, and the site's has them again")
}
}
func TestHTTPDRejects(t *testing.T) {
f, e := ready(t)
f.rejects = true
c := byTitle(t, setup.Run(e, options(f)), "httpd accepts its configuration")
if c.Status != setup.Fix || !strings.Contains(c.Detail, "syntax error") {
t.Errorf("httpd -n failing: %v %s", c.Status, c.Detail)
}
}
func TestRendererCheckFails(t *testing.T) {
f, e := ready(t)
f.checkFails = true
c := byTitle(t, setup.Run(e, options(f)), "repo-render check, as git")
if c.Status != setup.Fix || !strings.Contains(c.Detail, "plain text") {
t.Errorf("repo-render check failing: %v %s", c.Status, c.Detail)
}
}
func TestNotRendered(t *testing.T) {
f, e := ready(t)
os.Remove(f.path("/var/www/htdocs/code.example.com/index.html"))
c := byTitle(t, setup.Run(e, options(f)), "the site is rendered")
if want := []string{"su -s /bin/sh git -c 'repo-render render --all'"}; !slices.Equal(c.Fix, want) {
t.Errorf("render fix %q, want %q", c.Fix, want)
}
}
func TestServing(t *testing.T) {
for _, tc := range []struct {
name string
running bool
status int
fix string
}{
{"stopped", false, 200, "rcctl start httpd"},
{"old configuration", true, 404, "rcctl reload httpd"},
} {
f, e := ready(t)
f.running, f.status = tc.running, tc.status
c := byTitle(t, setup.Run(e, options(f)), "httpd serves the site")
if c.Status != setup.Fix || !slices.Equal(c.Fix, []string{tc.fix}) {
t.Errorf("%s: %v %q, want %q", tc.name, c.Status, c.Fix, tc.fix)
}
}
}
func TestNotRootCannotTell(t *testing.T) {
f, e := ready(t)
e.AsRoot = false
if c := byTitle(t, setup.Run(e, options(f)), "httpd accepts its configuration"); c.Status != setup.Unknown {
t.Errorf("httpd -n without root is %v, want unknown", c.Status)
}
}
func TestWriteShare(t *testing.T) {
var b bytes.Buffer
if err := setup.WriteShare(&b, share); err != nil {
t.Fatal(err)
}
got := map[string]int64{}
tr := tar.NewReader(&b)
for {
h, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
t.Fatal(err)
}
if h.Uname != "root" {
t.Errorf("%s is %s's", h.Name, h.Uname)
}
got[h.Name] = h.Mode
}
want := map[string]int64{"hooks/": 0o755, "hooks/post-receive": 0o755, "lexers/": 0o755, "lexers/tomo.xml": 0o644, "repo-render.yaml": 0o644}
for name, mode := range want {
if m, ok := got[name]; !ok || m != mode {
t.Errorf("%s: mode %o (there: %v), want %o", name, m, ok, mode)
}
}
if len(got) != len(want) {
t.Errorf("archive has %v", got)
}
}
func TestOnlyKnownCommandsRun(t *testing.T) {
if _, err := setup.RealRunner(&setup.Env{}, setup.Cmd{Argv: []string{"rm", "-rf", "/"}}); err == nil || !strings.Contains(err.Error(), "not one of the commands") {
t.Errorf("running rm: %v", err)
}
}
func TestReportWords(t *testing.T) {
checks := []*setup.Check{
{Title: "git", Status: setup.OK, Detail: "/usr/local/bin/git"},
{Title: "git account", Status: setup.Fix, Detail: "does not exist", Fix: []string{"useradd git"}},
{Title: "httpd serves the site", Status: setup.Later, Detail: "checked once the above are fixed"},
}
var plain, color bytes.Buffer
if n := setup.Report(&plain, checks, false); n != 2 {
t.Errorf("%d not done, want 2", n)
}
for _, want := range []string{" OK git ", " FIX git account ", "\n useradd git\n", " WAIT httpd serves", "1 OK, 1 to fix, 1 waiting on those.", "under each FIX as root"} {
if !strings.Contains(plain.String(), want) {
t.Errorf("report lacks %q:\n%s", want, plain.String())
}
}
if strings.ContainsAny(plain.String(), "\x1b✓✗·") {
t.Errorf("plain report has color or non-ASCII marks:\n%s", plain.String())
}
setup.Report(&color, checks, true)
for _, want := range []string{"\x1b[32mOK \x1b[0m", "\x1b[1;31mFIX \x1b[0m", "\x1b[90mWAIT\x1b[0m"} {
if !strings.Contains(color.String(), want) {
t.Errorf("colored report lacks %q", want)
}
}
}
func TestNologinShell(t *testing.T) {
f, e := ready(t)
f.write("/etc/passwd", "root:*:0:0::/root:/bin/ksh\ngit:*:1001:1001::/var/git:/sbin/nologin\n", 0o644, "")
c := byTitle(t, setup.Run(e, options(f)), "git account")
if !strings.Contains(c.Detail, "cannot take pushes") || strings.Contains(c.Detail, "log in") {
t.Errorf("nologin shell: %s", c.Detail)
}
}
func TestHomeLeftBehind(t *testing.T) {
f, e := fresh(t)
f.write("/etc/passwd", "root:*:0:0::/root:/bin/ksh\ngit:*:1001:1001::/var/www/pub/git:/sbin/nologin\n", 0o644, "")
f.mkdir("/var/www/pub/git/a.git", 0o755, "git:git")
f.mkdir("/var/www/pub/git/b.git", 0o755, "git:git")
f.write("/var/www/pub/git/.ssh/authorized_keys", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA bruce@laptop\n", 0o600, "git:git")
checks := setup.Run(e, options(f))
c := byTitle(t, checks, "git account")
if !strings.Contains(c.Detail, "leaves behind 2 repositories and its .ssh") || !strings.Contains(c.Detail, "git_dir: /var/www/pub/git") {
t.Errorf("moving git's home: %s", c.Detail)
}
c = byTitle(t, checks, "SSH keys that may push")
if !slices.Contains(c.Fix, "cat /var/www/pub/git/.ssh/authorized_keys >> /var/git/.ssh/authorized_keys") {
t.Errorf("keys do not come from git's old home: %q", c.Fix)
}
}
func TestKeysWithNoneToCopy(t *testing.T) {
f, e := fresh(t)
os.Remove(f.path("/root/.ssh/authorized_keys"))
c := byTitle(t, setup.Run(e, options(f)), "SSH keys that may push")
want := []string{"install -d -o git -g git -m 700 /var/git/.ssh", "install -o git -g git -m 600 /dev/null /var/git/.ssh/authorized_keys"}
if !slices.Equal(c.Fix, want) || !strings.Contains(c.Detail, "one per line") {
t.Errorf("keys with none to copy: %q (%s), want %q", c.Fix, c.Detail, want)
}
}
func TestKeysOfUserRunningIt(t *testing.T) {
f, e := fresh(t)
f.env["USER"] = "bruce"
f.write("/home/bruce/.ssh/authorized_keys", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA bruce@laptop\n", 0o600, "")
c := byTitle(t, setup.Run(e, options(f)), "SSH keys that may push")
if !slices.Contains(c.Fix, "cat /home/bruce/.ssh/authorized_keys >> /var/git/.ssh/authorized_keys") {
t.Errorf("keys do not come from the user running it: %q", c.Fix)
}
}
|