package main import ( "bytes" "log" "os" "path/filepath" "strings" "testing" ) func TestSplitRepo(t *testing.T) { f := testForge(t) author := f.store.forgeAuthor() for _, path := range []string{"notes", "infra", "infra/dns"} { repo := Repo{Visibility: visPublic, Issues: featDisabled, Wiki: featDisabled, Labels: []string{}, Grants: map[string]string{}} if err := f.store.CreateRepo(path, repo, author); err != nil { t.Fatal(err) } } cases := []struct{ url, repo, kind, rest string }{ {"/notes", "notes", "", ""}, {"/notes/", "notes", "", ""}, {"/notes/settings", "notes", "", "settings"}, {"/infra/dns", "infra/dns", "", ""}, // the section wins over the repo {"/infra/dns/log/", "infra/dns", "", "log/"}, // and keeps winning deeper {"/infra/other", "infra", "", "other"}, {"/infra/dns.issues/log/", "infra/dns", "issues", "log/"}, {"/notes.wiki/tree/", "notes", "wiki", "tree/"}, {"/nothing", "", "", ""}, } for _, c := range cases { repo, kind, rest, ok := f.store.splitRepo(c.url) if repo != c.repo || kind != c.kind || rest != c.rest || ok != (c.repo != "") { t.Errorf("%s resolved to %q %q %q %v", c.url, repo, kind, rest, ok) } } } // One process per datadir is a correctness rule: two lose updates to // users.json with nothing in any log to say so. func TestDatadirLockIsExclusive(t *testing.T) { dir := t.TempDir() unlock, err := lockDatadir(dir) if err != nil { t.Fatalf("the first lock was refused: %v", err) } if _, err := lockDatadir(dir); err == nil { t.Error("a second process took a datadir that was already being served") } if _, err := lockDatadir(t.TempDir()); err != nil { t.Errorf("a different datadir was refused: %v", err) } unlock() if unlock, err = lockDatadir(dir); err != nil { t.Errorf("the lock outlived the process that held it: %v", err) } unlock() } // A directory whose name could never be a repository path must not be // servable, and must not stop the forge starting either. func TestStrayDirectoryIsSkipped(t *testing.T) { dir := t.TempDir() s, err := openStore(dir) if err != nil { t.Fatal(err) } for _, name := range []string{"Uppercase.git", "with space.git", "settings.git"} { if err := os.MkdirAll(filepath.Join(s.reposDir, name), 0700); err != nil { t.Fatal(err) } } s, err = openStore(dir) if err != nil { t.Fatalf("a stray directory stopped the forge starting: %v", err) } if repos := s.Repos(); len(repos) != 0 { t.Errorf("a directory that is not a usable path is being served: %v", repos) } } // The scan exists to report what arrived by hand, and a report buried in noise // is not a report: a repository owns two sibling directories, and reading // either as a section warns about every file git keeps inside it. func TestScanIsQuietOnAHealthyDatadir(t *testing.T) { f := testForge(t) addRepo(t, f, "notes", Repo{Visibility: visPublic, Issues: featUsers, Wiki: featUsers}) addRepo(t, f, "infra/dns", Repo{Visibility: visPublic, Issues: featUsers, Wiki: featUsers}) var buf bytes.Buffer scan := func() string { t.Helper() buf.Reset() old := log.Writer() log.SetOutput(&buf) err := f.store.scanRepos() log.SetOutput(old) if err != nil { t.Fatal(err) } return strings.TrimSpace(buf.String()) } if out := scan(); out != "" { t.Errorf("a healthy datadir warned about itself:\n%s", out) } for _, path := range []string{"notes", "infra/dns"} { if _, ok := f.store.Repo(path); !ok { t.Errorf("%s did not survive the scan", path) } } // And the one thing it is for is still said. if err := os.MkdirAll(filepath.Join(f.store.reposDir, "Uppercase.git"), 0700); err != nil { t.Fatal(err) } if out := scan(); !strings.Contains(out, "Uppercase") { t.Errorf("a stray directory was not reported:\n%s", out) } } // The scan is the one place that reports a name nothing has validated, so a // directory can be named to look like the next line of the log. func TestTheScanCannotBeMadeToForgeALogLine(t *testing.T) { f := testForge(t) // No slash: this has to be one name on disk, not a path. forged := "innocent\n2026-08-22 00:00:00 203.0.113.9 GET x 200 0 1ms" repos := f.store.reposDir // One of each thing the scan reports: a stray file, a directory whose name // is not a usable path, and a stray file inside a section. if err := os.WriteFile(filepath.Join(repos, forged), nil, 0600); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(repos, forged+".git"), 0700); err != nil { t.Fatal(err) } if err := os.MkdirAll(filepath.Join(repos, "sect"), 0700); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(repos, "sect", forged), nil, 0600); err != nil { t.Fatal(err) } var buf bytes.Buffer old := log.Writer() log.SetOutput(&buf) err := f.store.scanRepos() log.SetOutput(old) if err != nil { t.Fatal(err) } out := strings.TrimRight(buf.String(), "\n") lines := strings.Split(out, "\n") if len(lines) != 3 { t.Fatalf("three strays wrote %d lines:\n%s", len(lines), out) } for _, line := range lines { if !strings.Contains(line, "repos: ") || !strings.Contains(line, `\n`) { t.Errorf("the line break was not escaped: %q", line) } } } // Every single-account edit found the account before the write. Writing an // absent key back would put a record in users.json under a name nothing else // knows about, with an origin the next start refuses to load. func TestEditingAnAccountThatIsGoneWritesNothing(t *testing.T) { dir := t.TempDir() store, err := openStore(dir) if err != nil { t.Fatal(err) } ran := false err = store.UpdateUser("ghost", "users: sign out every session for ghost", store.forgeAuthor(), func(acct *User) error { ran = true acct.Epoch++ return nil }) if err != nil { t.Fatalf("editing an account that is gone: %v", err) } if ran { t.Error("the edit ran against an account that is not there") } if _, ok := store.User("ghost"); ok { t.Fatal("the account was recreated") } // The consequence that matters: the datadir still starts. if _, err := openStore(dir); err != nil { t.Errorf("a fresh start refuses the datadir: %v", err) } }