package repo import ( "context" "os" "path/filepath" "strings" "testing" "time" "github.com/barerepo/server/internal/gitx" ) // TestDirRefusesEscape covers the oldest attack there is, from chapter 45.4's list. func TestDirRefusesEscape(t *testing.T) { root := t.TempDir() bad := [][2]string{ {"john", "../../etc"}, {"../../etc", "johnbot"}, {"john", ".."}, {"john", "a/b"}, {"john", ""}, {"john", "John"}, {"admin", "johnbot"}, {"", "johnbot"}, {"john", "sub/../../../../etc"}, } for _, c := range bad { if dir, err := Dir(root, c[0], c[1]); err == nil { t.Errorf("Dir(%q, %q) = %q, want an error", c[0], c[1], dir) } } dir, err := Dir(root, "john", "johnbot") if err != nil { t.Fatalf("Dir: %v", err) } // A reserved name is a route only at the top level, so john/runner is somebody's repository. for _, name := range []string{"runner", "admin", "new", "raw"} { if _, err := Dir(root, "john", name); err != nil { t.Errorf("Dir(john, %q) = %v, want a repository named after a route to be allowed", name, err) } } if want := filepath.Join(root, "john", "johnbot.git"); dir != want { t.Errorf("Dir = %q, want %q", dir, want) } } func TestCreate(t *testing.T) { if _, err := gitx.Version(context.Background()); err != nil { t.Skip("git is not installed") } ctx := context.Background() root := t.TempDir() // Rule 6: HEAD is what was asked for, never a hardcoded name or git's local default. dir, err := Create(ctx, root, "john", "johnbot", "trunk", "/usr/local/bin/barerepo") if err != nil { t.Fatalf("Create: %v", err) } if b, err := HeadBranch(ctx, dir); err != nil || b != "trunk" { t.Errorf("HeadBranch = %q, %v, want trunk", b, err) } if !IsEmpty(ctx, dir) { t.Error("a new repository is not empty") } if !Exists(root, "john", "johnbot") { t.Error("Exists = false after Create") } if _, err := Create(ctx, root, "john", "johnbot", "trunk", ""); err == nil { t.Error("creating the same repository twice was allowed") } // Chapter 41.5: two hooks, no samples, no update hook. entries, err := os.ReadDir(filepath.Join(dir, "hooks")) if err != nil { t.Fatal(err) } var names []string for _, e := range entries { names = append(names, e.Name()) } if strings.Join(names, ",") != "post-receive,pre-receive,proc-receive" { t.Errorf("hooks = %v, want exactly pre-receive, post-receive and proc-receive", names) } // The value is a prefix, and a glob matches nothing and fails silently. got, err := gitx.Run(ctx, dir, "config", "--get", "receive.procReceiveRefs") if err != nil || strings.TrimSpace(got) != "refs/proposals" { t.Errorf("receive.procReceiveRefs = %q, %v; want refs/proposals", got, err) } body, err := os.ReadFile(filepath.Join(dir, "hooks", "pre-receive")) if err != nil { t.Fatal(err) } if want := "#!/bin/sh\nexec /usr/local/bin/barerepo hook pre-receive\n"; string(body) != want { t.Errorf("pre-receive = %q, want %q", body, want) } if fi, _ := os.Stat(filepath.Join(dir, "hooks", "pre-receive")); fi.Mode()&0o100 == 0 { t.Error("pre-receive is not executable") } } // A ref name that is really a git argument must never reach a command. func TestCreateRefusesFlagAsBranch(t *testing.T) { if _, err := gitx.Version(context.Background()); err != nil { t.Skip("git is not installed") } root := t.TempDir() for _, branch := range []string{"--upload-pack=/bin/sh", "-x", "a b", "a..b"} { if _, err := Create(context.Background(), root, "john", "johnbot", branch, ""); err == nil { t.Errorf("branch %q was accepted", branch) } } } // A deleted repository waits out its window with the name still claimed. Chapter 44.4. func TestTrash(t *testing.T) { if _, err := gitx.Version(context.Background()); err != nil { t.Skip("git is not installed") } root := t.TempDir() ctx := context.Background() if _, err := Create(ctx, root, "john", "johnbot", "master", ""); err != nil { t.Fatal(err) } now := time.Now() if _, err := Trash(root, "john", "johnbot", now); err != nil { t.Fatal(err) } if Exists(root, "john", "johnbot") { t.Error("the repository is still being served") } if !InTrash(root, "john", "johnbot") { t.Error("the name is not held while the window runs") } // Nothing inside the window is erased. if n, err := EmptyTrash(root, 30*24*time.Hour, now); err != nil || n != 0 { t.Errorf("erased %d inside the window (%v)", n, err) } if !InTrash(root, "john", "johnbot") { t.Error("the copy was erased early") } // Past it, it goes. if n, err := EmptyTrash(root, 30*24*time.Hour, now.Add(31*24*time.Hour)); err != nil || n != 1 { t.Errorf("erased %d past the window (%v)", n, err) } if InTrash(root, "john", "johnbot") { t.Error("the copy outlived the window") } } func TestMove(t *testing.T) { if _, err := gitx.Version(context.Background()); err != nil { t.Skip("git is not installed") } root := t.TempDir() ctx := context.Background() if _, err := Create(ctx, root, "john", "johnbot", "master", ""); err != nil { t.Fatal(err) } if err := Move(root, "john", "johnbot", "lisa", "johnbot"); err != nil { t.Fatal(err) } if Exists(root, "john", "johnbot") || !Exists(root, "lisa", "johnbot") { t.Error("the directory did not move") } // Moving onto a name that exists must not clobber it. if _, err := Create(ctx, root, "john", "other", "master", ""); err != nil { t.Fatal(err) } if err := Move(root, "john", "other", "lisa", "johnbot"); err == nil { t.Error("a move over an existing repository was allowed") } } // A copy borrows objects, so Detach is what stops a delete taking its history too. func TestCopyAndDetach(t *testing.T) { ctx := context.Background() if _, err := gitx.Version(ctx); err != nil { t.Skip("git is not installed") } root := t.TempDir() src, err := Create(ctx, root, "john", "johnbot", "master", "") if err != nil { t.Fatal(err) } // One commit, and a proposal ref that must not travel. blob, err := gitx.RunStdin(ctx, src, "hello\n", "hash-object", "-w", "--stdin") if err != nil { t.Fatal(err) } tree, err := gitx.RunStdin(ctx, src, "100644 blob "+strings.TrimSpace(blob)+"\tREADME\n", "mktree") if err != nil { t.Fatal(err) } commit, err := gitx.RunStdin(ctx, src, "", "commit-tree", strings.TrimSpace(tree), "-m", "first") if err != nil { t.Fatal(err) } sha := strings.TrimSpace(commit) for _, ref := range []string{"refs/heads/master", "refs/proposals/1", "refs/notes/threads/1"} { if _, err := gitx.Run(ctx, src, "update-ref", ref, sha); err != nil { t.Fatal(err) } } if _, err := Copy(ctx, root, "john", "johnbot", "lisa", "johnbot", ""); err != nil { t.Fatal(err) } dst, _ := Dir(root, "lisa", "johnbot") // Chapter 21.1: history and notes come across, proposals do not. for _, want := range []string{"refs/heads/master", "refs/notes/threads/1"} { if _, err := gitx.Run(ctx, dst, "rev-parse", "--verify", "--quiet", want); err != nil { t.Errorf("%s did not come across", want) } } if _, err := gitx.Run(ctx, dst, "rev-parse", "--verify", "--quiet", "refs/proposals/1"); err == nil { t.Error("a proposal ref came across; it belongs to the original conversation") } // It borrows, which is what makes the copy instant. if Dependents(root, src) == nil { t.Error("the copy does not show as depending on the source") } if err := Detach(ctx, dst); err != nil { t.Fatal(err) } if Dependents(root, src) != nil { t.Error("the copy still borrows after detaching") } // And the history survives the source going away. if err := os.RemoveAll(src); err != nil { t.Fatal(err) } if _, err := gitx.Run(ctx, dst, "cat-file", "-e", sha+"^{commit}"); err != nil { t.Errorf("the copy lost its history when the source went: %v", err) } } // Chapter 26: repack after bulk ref deletion, or the pack files retain everything just deleted. func TestCollectPacksWhatIsLooseAndKeepsWhatIsReachable(t *testing.T) { ctx := context.Background() if _, err := gitx.Version(ctx); err != nil { t.Skip("git is not installed") } root := t.TempDir() dir, err := Create(ctx, root, "john", "johnbot", "master", "") if err != nil { t.Fatal(err) } blob, err := gitx.RunStdin(ctx, dir, "hello\n", "hash-object", "-w", "--stdin") if err != nil { t.Fatal(err) } tree, err := gitx.RunStdin(ctx, dir, "100644 blob "+strings.TrimSpace(blob)+"\tREADME\n", "mktree") if err != nil { t.Fatal(err) } commit, err := gitx.RunStdin(ctx, dir, "", "commit-tree", strings.TrimSpace(tree), "-m", "first") if err != nil { t.Fatal(err) } sha := strings.TrimSpace(commit) if _, err := gitx.Run(ctx, dir, "update-ref", "refs/heads/master", sha); err != nil { t.Fatal(err) } packs, err := filepath.Glob(filepath.Join(dir, "objects", "pack", "*.pack")) if err != nil { t.Fatal(err) } if len(packs) != 0 { t.Fatalf("the repository is packed before a collect, so this test proves nothing") } if err := Collect(ctx, dir, true); err != nil { t.Fatal(err) } packs, err = filepath.Glob(filepath.Join(dir, "objects", "pack", "*.pack")) if err != nil { t.Fatal(err) } if len(packs) == 0 { t.Error("a collect after a deletion did not repack, so the packs keep what was deleted") } // What a ref still reaches must survive, which is the half a repack must not get wrong. if _, err := gitx.Run(ctx, dir, "cat-file", "-e", sha); err != nil { t.Errorf("the commit master points at did not survive the repack: %v", err) } // The scheduled form is git's own --auto, which must be safe to run on a repository at rest. if err := Collect(ctx, dir, false); err != nil { t.Errorf("an automatic collect failed on a quiet repository: %v", err) } }