aboutsummaryrefslogtreecommitdiff
path: root/store_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'store_test.go')
-rw-r--r--store_test.go195
1 files changed, 195 insertions, 0 deletions
diff --git a/store_test.go b/store_test.go
new file mode 100644
index 0000000..9ae8c58
--- /dev/null
+++ b/store_test.go
@@ -0,0 +1,195 @@
+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)
+ }
+}