arnaucube
arnaucube/gitviewPublic Self-hosted repository
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}