clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / tinygit / browse_test.go
browse_test.go 6.5K
1package tinygit23import (4 "bytes"5 "context"6 "os"7 "os/exec"8 "path/filepath"9 "strings"10 "testing"11)1213func browseFixture(t *testing.T) (*GitRelay, Repository, string) {14 t.Helper()15 root := t.TempDir()16 r := Repository{Owner: strings.Repeat("a", 64), Identifier: "browse", Head: "refs/heads/main"}17 g := &GitRelay{root: root, repos: map[string]Repository{}}18 dir := g.repoPath(r)19 if err := os.MkdirAll(dir, 0700); err != nil {20 t.Fatal(err)21 }22 run := func(args ...string) string {23 t.Helper()24 c := exec.Command("git", append([]string{"-C", dir}, args...)...)25 c.Env = append(os.Environ(), "GIT_AUTHOR_NAME=Browser Test", "GIT_AUTHOR_EMAIL=test@example.test", "GIT_COMMITTER_NAME=Browser Test", "GIT_COMMITTER_EMAIL=test@example.test")26 b, err := c.CombinedOutput()27 if err != nil {28 t.Fatalf("git %v: %v %s", args, err, b)29 }30 return strings.TrimSpace(string(b))31 }32 run("init", "-b", "main")33 for name, body := range map[string]string{"source.go": "package main\n// <script>alert(1)</script>\n", "[literal]*.txt": "literal path", "large.txt": strings.Repeat("x", 300*1024), "binary.bin": "a\x00b"} {34 if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0600); err != nil {35 t.Fatal(err)36 }37 }38 run("add", ".")39 run("commit", "-m", "first source")40 first := run("rev-parse", "HEAD")41 if err := os.WriteFile(filepath.Join(dir, "source.go"), []byte("package main\n// second\n"), 0600); err != nil {42 t.Fatal(err)43 }44 run("commit", "-am", "second source")45 r.Refs = map[string]string{"refs/heads/main": run("rev-parse", "HEAD")}46 g.repos[key(r.Owner, r.Identifier)] = r47 // Browsing normally uses a bare repository; this fixture switches --git-dir to .git.48 g.root = filepath.Join(root, "bare")49 if err := os.MkdirAll(filepath.Dir(g.repoPath(r)), 0700); err != nil {50 t.Fatal(err)51 }52 cmd := exec.Command("git", "clone", "--bare", dir, g.repoPath(r))53 if b, err := cmd.CombinedOutput(); err != nil {54 t.Fatalf("bare clone: %v %s", err, b)55 }56 return g, r, first57}5859func TestBrowseRepositorySourceAndHistory(t *testing.T) {60 g, r, first := browseFixture(t)61 ctx := context.Background()62 q := BrowseRequest{Owner: r.Owner, Repo: r.Identifier}63 page, err := g.Browse(ctx, q)64 if err != nil || len(page.Entries) != 4 {65 t.Fatalf("tree: %+v %v", page, err)66 }67 q.View, q.Path, q.Ref = "file", "[literal]*.txt", first68 page, err = g.Browse(ctx, q)69 if err != nil || page.Content != "literal path" {70 t.Fatalf("literal: %+v %v", page, err)71 }72 q.Path = "source.go"73 page, err = g.Browse(ctx, q)74 if err != nil || !strings.Contains(page.Content, "<script>") {75 t.Fatalf("historical source: %+v %v", page, err)76 }77 q.View, q.Ref, q.Path, q.Limit = "history", "main", "", 178 page, err = g.Browse(ctx, q)79 if err != nil || len(page.Commits) != 1 || page.NextOffset != 1 {80 t.Fatalf("history: %+v %v", page, err)81 }82 q.View = "commit"83 page, err = g.Browse(ctx, q)84 if err != nil || !strings.Contains(page.Diff, "+// second") {85 t.Fatalf("diff: %+v %v", page, err)86 }87}8889func TestFetchMissingUsesCompleteLocalObjects(t *testing.T) {90 g, repo, _ := browseFixture(t)91 expected := map[string]string{"refs/heads/repaired": repo.Refs["refs/heads/main"]}92 if err := g.FetchMissing(context.Background(), repo, nil, expected); err != nil {93 t.Fatalf("complete local inventory should not need a network source: %v", err)94 }95 native, err := g.nativeRefs(repo)96 if err != nil || native["refs/heads/repaired"] != expected["refs/heads/repaired"] {97 t.Fatalf("locally available objects were not projected to expected refs: %v %v", native, err)98 }99}100101func TestBrowseRejectsUnsafeOrUnpublishedObjects(t *testing.T) {102 g, r, _ := browseFixture(t)103 cmd := exec.Command("git", "--git-dir", g.repoPath(r), "hash-object", "-w", "--stdin")104 cmd.Stdin = strings.NewReader("unpublished secret")105 id, err := cmd.Output()106 if err != nil {107 t.Fatal(err)108 }109 for _, q := range []BrowseRequest{110 {Owner: r.Owner, Repo: r.Identifier, Ref: "--output=/tmp/nope"},111 {Owner: r.Owner, Repo: r.Identifier, Ref: strings.TrimSpace(string(id))},112 {Owner: r.Owner, Repo: r.Identifier, Path: "../config"},113 {Owner: r.Owner, Repo: r.Identifier, Path: "/etc/passwd"},114 } {115 if _, err := g.Browse(context.Background(), q); err == nil {116 t.Fatalf("accepted unsafe request: %+v", q)117 }118 }119}120121func TestBrowseFilePreviewIsBoundedAndBinaryAware(t *testing.T) {122 g, r, _ := browseFixture(t)123 for _, name := range []string{"large.txt", "binary.bin"} {124 page, err := g.Browse(context.Background(), BrowseRequest{Owner: r.Owner, Repo: r.Identifier, View: "file", Path: name})125 if err != nil {126 t.Fatal(err)127 }128 if name == "large.txt" && (!page.Truncated || len(page.Content) > 256*1024) {129 t.Fatal("unbounded preview")130 }131 if name == "binary.bin" && (!page.Binary || page.Content != "") {132 t.Fatal("binary shown as source")133 }134 }135}136137func TestBrowseHiddenCommitAndFullBinaryDownload(t *testing.T) {138 g, r, _ := browseFixture(t)139 cmd := g.browseCommand(context.Background(), r, "rev-parse", "HEAD^{tree}")140 tree, err := cmd.Output()141 if err != nil {142 t.Fatal(err)143 }144 cmd = g.browseCommand(context.Background(), r, "commit-tree", strings.TrimSpace(string(tree)))145 cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=test", "GIT_AUTHOR_EMAIL=test@example.test", "GIT_COMMITTER_NAME=test", "GIT_COMMITTER_EMAIL=test@example.test")146 cmd.Stdin = strings.NewReader("unpublished commit")147 hidden, err := cmd.Output()148 if err != nil {149 t.Fatal(err)150 }151 oid := strings.TrimSpace(string(hidden))152 if err := g.browseCommand(context.Background(), r, "update-ref", "refs/tinyrelay/pending/secret", oid).Run(); err != nil {153 t.Fatal(err)154 }155 if _, err := g.Browse(context.Background(), BrowseRequest{Owner: r.Owner, Repo: r.Identifier, Ref: oid}); err == nil {156 t.Fatal("unpublished commit exposed")157 }158 var result bytes.Buffer159 if err := g.WriteSource(context.Background(), BrowseRequest{Owner: r.Owner, Repo: r.Identifier, Path: "large.txt"}, &result); err != nil {160 t.Fatal(err)161 }162 if result.Len() != 300*1024 {163 t.Fatalf("download truncated: %d", result.Len())164 }165}166167func TestBrowseTreePaginationHasNoMissingEntries(t *testing.T) {168 g, r, _ := browseFixture(t)169 names := map[string]bool{}170 q := BrowseRequest{Owner: r.Owner, Repo: r.Identifier, Limit: 1}171 for {172 page, err := g.Browse(context.Background(), q)173 if err != nil {174 t.Fatal(err)175 }176 for _, entry := range page.Entries {177 if names[entry.Name] {178 t.Fatal("duplicate entry")179 }180 names[entry.Name] = true181 }182 if page.NextOffset < 0 {183 break184 }185 q.Offset = page.NextOffset186 }187 if len(names) != 4 {188 t.Fatalf("missing entries: %v", names)189 }190}