gitview/internal/routes/routes_test.go
internal/routes/routes_test.goRaw
1package routes
2
3import (
4 "archive/tar"
5 "compress/gzip"
6 "io"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "os/exec"
11 "path/filepath"
12 "strings"
13 "testing"
14 "time"
15
16 gogit "github.com/go-git/go-git/v5"
17 "github.com/go-git/go-git/v5/plumbing"
18 "github.com/go-git/go-git/v5/plumbing/object"
19 "gitview/internal/config"
20)
21
22// testWorkspace uses real Git objects so the handlers exercise repository behavior.
23func testWorkspace(t *testing.T) (*config.Config, string) {
24 t.Helper()
25 scan := t.TempDir()
26 path := filepath.Join(scan, "sample")
27 repo, err := gogit.PlainInit(path, false)
28 if err != nil {
29 t.Fatal(err)
30 }
31 if err := repo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName("main"))); err != nil {
32 t.Fatal(err)
33 }
34 wt, err := repo.Worktree()
35 if err != nil {
36 t.Fatal(err)
37 }
38 for name, content := range map[string]string{"README.md": "# Sample project\n\nA useful README.\n<script>alert('unsafe')</script>", "src/main.go": "package main\n\nfunc main() {}\n", "plain.unknownextension": "hello <world>\n"} {
39 if err := os.MkdirAll(filepath.Dir(filepath.Join(path, name)), 0755); err != nil {
40 t.Fatal(err)
41 }
42 if err := os.WriteFile(filepath.Join(path, name), []byte(content), 0644); err != nil {
43 t.Fatal(err)
44 }
45 }
46 if _, err := wt.Add("."); err != nil {
47 t.Fatal(err)
48 }
49 signature := &object.Signature{Name: "Test Author", Email: "author@example.com", When: time.Date(2026, 1, 2, 12, 0, 0, 0, time.UTC)}
50 hash, err := wt.Commit("Initial project", &gogit.CommitOptions{Author: signature})
51 if err != nil {
52 t.Fatal(err)
53 }
54 if _, err := repo.CreateTag("v1.0.0", hash, nil); err != nil {
55 t.Fatal(err)
56 }
57 if err := repo.Storer.SetReference(plumbing.NewHashReference(plumbing.NewBranchReferenceName("develop"), hash)); err != nil {
58 t.Fatal(err)
59 }
60 if err := os.WriteFile(filepath.Join(path, "description"), []byte("A sample repository"), 0644); err != nil {
61 t.Fatal(err)
62 }
63 c := &config.Config{}
64 c.Repo.ScanPath = scan
65 c.Repo.MainBranch = []string{"main", "master"}
66 c.Repo.Readme = []string{"README.md"}
67 c.Dirs.Templates, _ = filepath.Abs("../../templates")
68 c.Dirs.Static, _ = filepath.Abs("../../static")
69 c.Meta.Title = "Gitview"
70 c.Meta.SyntaxHighlight = "github"
71 return c, hash.String()
72}
73
74func TestRepositoryBrowsing(t *testing.T) {
75 c, hash := testWorkspace(t)
76 handler := Handlers(c)
77 for _, tt := range []struct {
78 path, contains string
79 status int
80 }{
81 {"/", "sample", 200}, {"/sample", "Sample project", 200},
82 {"/sample/tree/main/", "README.md", 200}, {"/sample/tree/main/src", "main.go", 200},
83 {"/sample/blob/main/src/main.go", "chroma", 200}, {"/sample/blob/main/plain.unknownextension", "hello", 200},
84 {"/sample/blob/main/src/main.go?raw=true", "package main", 200},
85 {"/sample/log/main", "Initial project", 200}, {"/sample/commit/" + hash, "diff-add", 200},
86 {"/sample/refs/", "v1.0.0", 200}, {"/static/app.js", "filterRepositories", 200},
87 {"/missing", "could not be found", 404}, {"/sample/tree/missing/", "could not be found", 404},
88 {"/sample/blob/main/missing", "could not be found", 404}, {"/sample/tree/main/missing", "could not be found", 404},
89 {"/sample/unknown", "could not be found", 404}, {"/unknown/path/again", "could not be found", 404},
90 } {
91 t.Run(tt.path, func(t *testing.T) {
92 w := httptest.NewRecorder()
93 handler.ServeHTTP(w, httptest.NewRequest("GET", tt.path, nil))
94 if w.Code != tt.status || !strings.Contains(w.Body.String(), tt.contains) {
95 t.Fatalf("status %d, expected %d containing %q; body: %.500s", w.Code, tt.status, tt.contains, w.Body.String())
96 }
97 })
98 }
99 w := httptest.NewRecorder()
100 handler.ServeHTTP(w, httptest.NewRequest("GET", "/sample", nil))
101 if strings.Contains(w.Body.String(), "<script>alert") {
102 t.Fatal("README contains executable script")
103 }
104 w = httptest.NewRecorder()
105 handler.ServeHTTP(w, httptest.NewRequest("GET", "/sample/blob/main/src/main.go?raw=true", nil))
106 if got := w.Header().Get("Content-Type"); got != "text/plain; charset=utf-8" {
107 t.Fatalf("raw content type: %s", got)
108 }
109}
110
111func TestArchiveContents(t *testing.T) {
112 c, _ := testWorkspace(t)
113 w := httptest.NewRecorder()
114 Handlers(c).ServeHTTP(w, httptest.NewRequest("GET", "/sample/archive/main.tar.gz", nil))
115 if w.Code != 200 {
116 t.Fatalf("archive status: %d", w.Code)
117 }
118 gz, err := gzip.NewReader(w.Body)
119 if err != nil {
120 t.Fatal(err)
121 }
122 defer gz.Close()
123 tr := tar.NewReader(gz)
124 found := false
125 for {
126 h, err := tr.Next()
127 if err == io.EOF {
128 break
129 }
130 if err != nil {
131 t.Fatal(err)
132 }
133 if h.Name == "sample-main/src/main.go" {
134 b, err := io.ReadAll(tr)
135 if err != nil {
136 t.Fatal(err)
137 }
138 found = string(b) == "package main\n\nfunc main() {}\n"
139 }
140 }
141 if !found {
142 t.Fatal("archive did not contain expected source")
143 }
144}
145
146func TestVisibilityAndPushPolicy(t *testing.T) {
147 c, _ := testWorkspace(t)
148 c.Repo.Unlisted = []string{"sample"}
149 handler := Handlers(c)
150 w := httptest.NewRecorder()
151 handler.ServeHTTP(w, httptest.NewRequest("GET", "/", nil))
152 if strings.Contains(w.Body.String(), "data-repo=\"sample\"") {
153 t.Fatal("unlisted repository is visible")
154 }
155 w = httptest.NewRecorder()
156 handler.ServeHTTP(w, httptest.NewRequest("GET", "/sample", nil))
157 if w.Code != 200 {
158 t.Fatal("unlisted repository is not accessible")
159 }
160 c.Repo.Ignore = []string{"sample"}
161 for _, path := range []string{"/sample", "/sample/info/refs?service=git-upload-pack", "/sample/archive/main.tar.gz"} {
162 w = httptest.NewRecorder()
163 handler.ServeHTTP(w, httptest.NewRequest("GET", path, nil))
164 if w.Code != 404 {
165 t.Fatalf("ignored repo accessible: %s (%d)", path, w.Code)
166 }
167 }
168 for _, method := range []string{"GET", "POST"} {
169 w = httptest.NewRecorder()
170 handler.ServeHTTP(w, httptest.NewRequest(method, "/sample/git-receive-pack", nil))
171 if w.Code != 400 {
172 t.Fatalf("push not rejected: %d", w.Code)
173 }
174 }
175}
176
177func TestSmartHTTPClone(t *testing.T) {
178 if _, err := exec.LookPath("git"); err != nil {
179 t.Skip("git executable unavailable")
180 }
181 c, hash := testWorkspace(t)
182 server := httptest.NewServer(Handlers(c))
183 defer server.Close()
184 target := filepath.Join(t.TempDir(), "clone")
185 cmd := exec.Command("git", "clone", server.URL+"/sample", target)
186 cmd.Env = append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=/dev/null", "GIT_TERMINAL_PROMPT=0")
187 if out, err := cmd.CombinedOutput(); err != nil {
188 t.Fatalf("clone: %v: %s", err, out)
189 }
190 repo, err := gogit.PlainOpen(target)
191 if err != nil {
192 t.Fatal(err)
193 }
194 head, err := repo.Head()
195 if err != nil {
196 t.Fatal(err)
197 }
198 if head.Hash().String() != hash {
199 t.Fatal("clone HEAD differs from source")
200 }
201 resp, err := http.Get(server.URL + "/sample/info/refs?extra=1&service=git-upload-pack")
202 if err != nil {
203 t.Fatal(err)
204 }
205 defer resp.Body.Close()
206 if resp.Header.Get("Content-Type") != "application/x-git-upload-pack-advertisement" {
207 t.Fatal("smart HTTP query routing failed")
208 }
209}
210
211func TestCustomDefaultBranchAndEmptyRepository(t *testing.T) {
212 c, hash := testWorkspace(t)
213 repo, err := gogit.PlainOpen(filepath.Join(c.Repo.ScanPath, "sample"))
214 if err != nil {
215 t.Fatal(err)
216 }
217 if err := repo.Storer.SetReference(plumbing.NewHashReference(plumbing.NewBranchReferenceName("trunk"), plumbing.NewHash(hash))); err != nil {
218 t.Fatal(err)
219 }
220 if err := repo.Storer.SetReference(plumbing.NewSymbolicReference(plumbing.HEAD, plumbing.NewBranchReferenceName("trunk"))); err != nil {
221 t.Fatal(err)
222 }
223 c.Repo.MainBranch = []string{"nonexistent"}
224 if _, err := gogit.PlainInit(filepath.Join(c.Repo.ScanPath, "empty"), false); err != nil {
225 t.Fatal(err)
226 }
227 handler := Handlers(c)
228 for _, path := range []string{"/", "/sample"} {
229 w := httptest.NewRecorder()
230 handler.ServeHTTP(w, httptest.NewRequest("GET", path, nil))
231 if w.Code != 200 {
232 t.Fatalf("%s: status %d", path, w.Code)
233 }
234 if path == "/sample" && !strings.Contains(w.Body.String(), "/sample/tree/trunk/") {
235 t.Fatal("default branch did not fall back to HEAD")
236 }
237 }
238}