package main import ( "encoding/json" "fmt" "net" "os" "path/filepath" "regexp" "slices" "strings" "syscall" "testing" "time" ) // ADR 0415, issue 1017: the sweep keeps ~/.ssh to what this module knows and moves the rest out. func sweepHome(t *testing.T) (*Client, string) { t.Helper() home := t.TempDir() dir := filepath.Join(home, ".ssh") put := func(rel, body string, mode os.FileMode) { p := filepath.Join(dir, rel) if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(p, []byte(body), mode); err != nil { t.Fatal(err) } } key := "-----BEGIN OPENSSH PRIVATE KEY-----\nPRIVATE\n" for rel := range Declared { if rel != "config.d" { put(rel, "declared\n", 0o600) } } put("id_ed25519", key, 0o600) put("id_ed25519.pub", ed25519Line(1, "me")+"\n", 0o644) put("work_key", key, 0o600) // a key with an unusual name put("work_key.pub", ed25519Line(2, "work")+"\n", 0o644) put("work_key-cert.pub", "ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29t work\n", 0o644) put("config.bak-20260807-015327", "old\n", 0o600) put("known_hosts.old", "old\n", 0o644) put("config.d/mesh", "Host ace\n", 0o600) put("removed-hal-20261002/config", "old\n", 0o600) put("removed-drone-20261004/id_drone", key, 0o600) // a key inside a leftover folder put("removed-drone-20261004/config", "old\n", 0o600) put("agent_only.pub", ed25519Line(3, "private half in an agent")+"\n", 0o644) put("notes.pub", "not a key\n", 0o644) put("authorized_keys.revoked-20261001T000000Z", ed25519Line(4, "revoked")+"\n", 0o600) put("authorized_keys.bak", ed25519Line(5, "copy")+"\n", 0o600) put("authorized_keys2", ed25519Line(6, "second file")+"\n", 0o600) put("config", "# BEGIN mesh ssh-client.config\nInclude ~/.ssh/config.d/*\n# END mesh ssh-client.config\n\nHost mine\n User me\nInclude ~/.ssh/config.d/mesh\n", 0o600) elsewhere := filepath.Join(home, "token") os.WriteFile(elsewhere, []byte(key), 0o600) if err := os.Symlink(elsewhere, filepath.Join(dir, "id_token")); err != nil { t.Fatal(err) } if err := os.Symlink(filepath.Join(home, "gone"), filepath.Join(dir, "dangling")); err != nil { t.Fatal(err) } return &Client{Home: home}, dir } func exists(p string) bool { _, err := os.Lstat(p); return err == nil } func TestSweepKeepsWhatItKnows(t *testing.T) { c, dir := sweepHome(t) now := time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC) s, err := c.Sweep(now) if err != nil { t.Fatal(err) } for _, rel := range []string{"config", "known_hosts", "authorized_keys", "config.d/00-mesh", "config.d/10-forge", KeyFile, KeyFile + ".pub", "id_ed25519", "id_ed25519.pub", "work_key", "work_key.pub", "work_key-cert.pub", "agent_only.pub", "removed-drone-20261004/id_drone", "authorized_keys2", "id_token"} { if !exists(filepath.Join(dir, rel)) { t.Errorf("%s was moved; it is known", rel) } } root := filepath.Join(c.Home, RetiredDir, "20261017T120000Z") for _, rel := range []string{"config.bak-20260807-015327", "known_hosts.old", "config.d/mesh", "removed-hal-20261002", "notes.pub", "removed-drone-20261004/config", "authorized_keys.revoked-20261001T000000Z", "authorized_keys.bak", "dangling"} { if exists(filepath.Join(dir, rel)) { t.Errorf("%s is still in ~/.ssh", rel) } if !exists(filepath.Join(root, rel)) { t.Errorf("%s is not kept at %s", rel, filepath.Join(root, rel)) } } if !exists(filepath.Join(root, "removed-hal-20261002", "config")) { t.Error("a folder was not moved whole") } if len(s.Moved) != 9 { t.Errorf("moved %d paths, want 9: %+v", len(s.Moved), s.Moved) } if raw, _ := os.ReadFile(filepath.Join(dir, "config")); !strings.Contains(string(raw), "Host mine\n User me\nInclude ~/.ssh/config.d/mesh\n") { t.Errorf("the operator's lines in config changed: %q", raw) } if st, _ := os.Stat(filepath.Join(c.Home, RetiredDir)); st == nil || st.Mode().Perm() != 0o700 { t.Errorf("the retired folder is not 0700: %v", st) } again, err := c.Sweep(now.Add(time.Minute)) if err != nil || len(again.Moved) != 0 { t.Fatalf("a second sweep moved %+v (err %v)", again.Moved, err) } if exists(filepath.Join(c.Home, RetiredDir, "20261017T120100Z")) { t.Error("a sweep that moved nothing made a folder") } retired, err := c.Retired() if err != nil || len(retired) != 1 || retired[0].At != "20261017T120000Z" || len(retired[0].Moved) != 9 { raw, _ := json.Marshal(retired) t.Fatalf("Retired = %s (err %v), want one sweep of 9 paths", raw, err) } for _, m := range retired[0].Moved { if !strings.HasPrefix(m.From, dir+"/") || !strings.HasPrefix(m.To, root+"/") || !exists(m.To) { t.Errorf("listed %+v, not a path moved from ~/.ssh into %s", m, root) } } } func TestSweepOnceSaysEachMove(t *testing.T) { c, dir := sweepHome(t) var said []string c.SweepOnce(time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC), func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }) want := "moved " + filepath.Join(dir, "known_hosts.old") + ", which this module does not know, to " + filepath.Join(c.Home, RetiredDir, "20261017T120000Z", "known_hosts.old") if !slices.Contains(said, want) || len(said) != 9 { t.Fatalf("said %q, want 9 lines with %q", said, want) } } func TestSweepLeavesSocketsAndUnreadable(t *testing.T) { c, dir := sweepHome(t) sock := filepath.Join(dir, "cm-ace") if l, err := net.Listen("unix", sock); err == nil { defer l.Close() } else { t.Skipf("no unix socket here: %v", err) } locked := filepath.Join(dir, "unreadable") os.WriteFile(locked, []byte("?"), 0o000) if f, err := os.Open(locked); err == nil { f.Close() t.Skip("running as a user who reads a mode-0 file") } s, err := c.Sweep(time.Now()) if err != nil { t.Fatal(err) } if !exists(sock) { t.Error("a socket was moved") } if !exists(locked) || len(s.Left) != 1 || s.Left[0] != locked { t.Errorf("an unreadable file was not left and named: left %v", s.Left) } } func TestSweepWithoutSSHDir(t *testing.T) { c := &Client{Home: t.TempDir()} if s, err := c.Sweep(time.Now()); err != nil || len(s.Moved) != 0 { t.Fatalf("Sweep = %+v, %v", s, err) } } // Every path the manifest declares under ~/.ssh is one the sweep knows. func TestDeclaredMatchesManifest(t *testing.T) { raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } var m struct { Resources []struct{ Path string } `json:"resources"` Facts map[string]struct{ Path string } } if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } under := regexp.MustCompile(`^(\$\{machine:account-home\}/)?\.ssh/(.+)$`) paths := []string{} for _, r := range m.Resources { paths = append(paths, r.Path) } for _, f := range m.Facts { paths = append(paths, f.Path) } seen := 0 for _, p := range paths { g := under.FindStringSubmatch(strings.TrimSuffix(p, "/")) if g == nil { continue } seen++ if !Declared[g[2]] { t.Errorf("module.json declares ~/.ssh/%s, which the sweep does not know: it would be moved out", g[2]) } } if seen < 5 { t.Errorf("found %d paths under ~/.ssh in module.json; the manifest's shape changed", seen) } } // Review round 1: a folder holding only a socket is gone into, not moved with the socket. func TestSweepKeepsAFolderHoldingASocket(t *testing.T) { home := t.TempDir() dir := filepath.Join(home, ".ssh", "cm") os.MkdirAll(dir, 0o700) l, err := net.Listen("unix", filepath.Join(dir, "s")) if err != nil { t.Skipf("no unix socket here: %v", err) } defer l.Close() c := &Client{Home: home} if s, err := c.Sweep(time.Now()); err != nil || len(s.Moved) != 0 || !exists(filepath.Join(dir, "s")) { t.Fatalf("moved %+v (err %v); the socket's folder must stay", s.Moved, err) } } // Review round 1: two sweeps in the same second never share a folder, so neither overwrites the other. func TestSweepsInOneSecondKeepBoth(t *testing.T) { home := t.TempDir() dir := filepath.Join(home, ".ssh") os.MkdirAll(dir, 0o700) c := &Client{Home: home} now := time.Date(2026, 10, 17, 12, 0, 0, 0, time.UTC) os.WriteFile(filepath.Join(dir, "a"), []byte("1"), 0o600) first, err := c.Sweep(now) if err != nil { t.Fatal(err) } os.WriteFile(filepath.Join(dir, "a"), []byte("2"), 0o600) second, err := c.Sweep(now) if err != nil { t.Fatal(err) } if first.At == second.At { t.Fatalf("both sweeps used %s", first.At) } for at, want := range map[string]string{first.At: "1", second.At: "2"} { if raw, _ := os.ReadFile(filepath.Join(home, RetiredDir, at, "a")); string(raw) != want { t.Errorf("%s/a = %q, want %q", at, raw, want) } } if r, _ := c.Retired(); len(r) != 2 { t.Errorf("Retired lists %d sweeps, want 2", len(r)) } } // Review round 1: a link to a pipe below ~/.ssh never blocks the sweep. func TestSweepDoesNotOpenAPipe(t *testing.T) { home := t.TempDir() sub := filepath.Join(home, ".ssh", "sub") os.MkdirAll(sub, 0o700) fifo := filepath.Join(home, "fifo") if err := syscall.Mkfifo(fifo, 0o600); err != nil { t.Skipf("no fifo here: %v", err) } os.Symlink(fifo, filepath.Join(sub, "p")) done := make(chan error, 1) go func() { _, err := (&Client{Home: home}).Sweep(time.Now()); done <- err }() select { case err := <-done: if err != nil { t.Fatal(err) } case <-time.After(2 * time.Second): t.Fatal("the sweep blocked on a pipe") } }