Parent directory

repository_browser_test.go

7171 bytes
  1package web
  2
  3import (
  4	"net/http"
  5	"net/http/httptest"
  6	"strings"
  7	"testing"
  8
  9	"github.com/charmbracelet/soft-serve/pkg/access"
 10	"github.com/charmbracelet/soft-serve/pkg/config"
 11	"github.com/charmbracelet/soft-serve/pkg/proto"
 12)
 13
 14func TestRepositoryBrowserRawSizeLimit(t *testing.T) {
 15	ctx, be, _ := newLFSTestContext(t)
 16	cfg := config.FromContext(ctx)
 17	cfg.HTTP.WebUI.Enabled = true
 18	allowKeyless := true
 19	cfg.AllowKeyless = &allowKeyless
 20	anonymous := access.ReadOnlyAccess
 21	cfg.AnonAccess = &anonymous
 22	owner, err := be.CreateUser(ctx, "owner", proto.UserOptions{})
 23	if err != nil {
 24		t.Fatal(err)
 25	}
 26	if _, err := be.CreateRepository(ctx, "large", owner, proto.RepositoryOptions{}); err != nil {
 27		t.Fatal(err)
 28	}
 29	commitHomepageFile(t, cfg.DataPath, "large", "large.bin", strings.Repeat("x", (16<<20)+1))
 30
 31	w := httptest.NewRecorder()
 32	NewRouter(ctx).ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/large/@/raw?path=large.bin", nil))
 33	if w.Code != http.StatusRequestEntityTooLarge {
 34		t.Fatalf("oversized raw response = %d: %s", w.Code, w.Body.String())
 35	}
 36}
 37
 38func TestRepositoryBrowserOverviewAndTreeRoutes(t *testing.T) {
 39	ctx, be, _ := newLFSTestContext(t)
 40	cfg := config.FromContext(ctx)
 41	cfg.HTTP.WebUI.Enabled = true
 42	cfg.SSH.PublicURL = "ssh://git@ssh.example"
 43	cfg.HTTP.PublicURL = "https://git.example"
 44	allowKeyless := true
 45	cfg.AllowKeyless = &allowKeyless
 46	anonymous := access.ReadOnlyAccess
 47	cfg.AnonAccess = &anonymous
 48
 49	owner, err := be.CreateUser(ctx, "owner", proto.UserOptions{})
 50	if err != nil {
 51		t.Fatal(err)
 52	}
 53	if _, err := be.CreateRepository(ctx, "team/nested", owner, proto.RepositoryOptions{
 54		ProjectName: "Nested project", Description: "A public repository",
 55	}); err != nil {
 56		t.Fatal(err)
 57	}
 58	if _, err := be.CreateRepository(ctx, "team/empty", owner, proto.RepositoryOptions{}); err != nil {
 59		t.Fatal(err)
 60	}
 61	if _, err := be.CreateRepository(ctx, "team/private", owner, proto.RepositoryOptions{Private: true}); err != nil {
 62		t.Fatal(err)
 63	}
 64	commitHomepageFile(t, cfg.DataPath, "team/nested", "README.md", "# Hello\n\n![icon](images/icon.png)\n")
 65	commitHomepageFile(t, cfg.DataPath, "team/nested", "dir/a.txt", "a")
 66	commitHomepageFile(t, cfg.DataPath, "team/nested", "dir/nested/b.txt", "b")
 67	commitHomepageFile(t, cfg.DataPath, "team/nested", "source.go", "<script>alert(1)</script>")
 68	commitHomepageFile(t, cfg.DataPath, "team/nested", "docs/guide.md", "# Guide\n\n[Source](../source.go)\n\n![Icon](../images/icon.png)\n\n<script>alert(1)</script>\n")
 69	commitHomepageFile(t, cfg.DataPath, "team/nested", "images/icon.png", string([]byte{'\x89', 'P', 'N', 'G'}))
 70	commitHomepageFile(t, cfg.DataPath, "team/nested", "unsafe.svg", "<svg></svg>")
 71	commitHomepageFile(t, cfg.DataPath, "team/nested", "unsafe.html", "<script></script>")
 72	commitHomepageFile(t, cfg.DataPath, "team/nested", "unknown.xyz", "unknown")
 73
 74	worktree := t.TempDir()
 75	runHomepageGit(t, "clone", cfg.DataPath+"/repos/team/nested.git", worktree)
 76	runHomepageGitIn(t, worktree, "config", "user.email", "test@example.com")
 77	runHomepageGitIn(t, worktree, "config", "user.name", "Test User")
 78	runHomepageGitIn(t, worktree, "checkout", "-qb", "feature/with-slash")
 79	runHomepageGitIn(t, worktree, "push", "origin", "HEAD")
 80
 81	router := NewRouter(ctx)
 82	request := func(target string) *httptest.ResponseRecorder {
 83		w := httptest.NewRecorder()
 84		router.ServeHTTP(w, httptest.NewRequest(http.MethodGet, target, nil))
 85		return w
 86	}
 87
 88	overview := request("/team/nested")
 89	if overview.Code != http.StatusOK {
 90		t.Fatalf("overview status = %d: %s", overview.Code, overview.Body.String())
 91	}
 92	for _, expected := range []string{"Nested project", "https://git.example/team/nested.git", "Copy HTTP clone URL", "/team/nested/@/tree", "/team/nested/@/raw?path=images%2Ficon.png"} {
 93		if !strings.Contains(overview.Body.String(), expected) {
 94			t.Errorf("overview missing %q: %s", expected, overview.Body.String())
 95		}
 96	}
 97
 98	empty := request("/team/empty")
 99	if empty.Code != http.StatusOK || !strings.Contains(empty.Body.String(), "repository is empty") {
100		t.Errorf("empty repository response = %d: %s", empty.Code, empty.Body.String())
101	}
102	private := request("/team/private")
103	if private.Code != http.StatusNotFound {
104		t.Errorf("private repository status = %d", private.Code)
105	}
106
107	root := request("/team/nested/@/tree?ref=refs%2Fheads%2Fmain")
108	if root.Code != http.StatusOK {
109		t.Fatalf("root tree status = %d: %s", root.Code, root.Body.String())
110	}
111	if strings.Index(root.Body.String(), ">dir<") > strings.Index(root.Body.String(), ">source.go<") {
112		t.Errorf("directories are not listed before files: %s", root.Body.String())
113	}
114	nested := request("/team/nested/@/tree?ref=refs%2Fheads%2Fmain&path=dir")
115	if nested.Code != http.StatusOK || strings.Index(nested.Body.String(), ">nested<") > strings.Index(nested.Body.String(), ">a.txt<") {
116		t.Errorf("nested tree ordering response = %d: %s", nested.Code, nested.Body.String())
117	}
118	source := request("/team/nested/@/tree?ref=refs%2Fheads%2Fmain&path=source.go")
119	if source.Code != http.StatusOK || strings.Contains(source.Body.String(), "<script>alert") || !strings.Contains(source.Body.String(), "&lt;") || !strings.Contains(source.Body.String(), `data-copy="https://git.example/team/nested/@/raw?path=source.go&amp;ref=refs%2Fheads%2Fmain"`) {
120		t.Errorf("source response = %d: %s", source.Code, source.Body.String())
121	}
122	markdown := request("/team/nested/@/tree?ref=refs%2Fheads%2Fmain&path=docs%2Fguide.md")
123	if markdown.Code != http.StatusOK || !strings.Contains(markdown.Body.String(), "<h1>Guide</h1>") || strings.Contains(markdown.Body.String(), "<script>alert") || !strings.Contains(markdown.Body.String(), "/team/nested/@/tree?path=source.go&amp;ref=refs%2Fheads%2Fmain") || !strings.Contains(markdown.Body.String(), "/team/nested/@/raw?path=images%2Ficon.png&amp;ref=refs%2Fheads%2Fmain") {
124		t.Errorf("markdown response = %d: %s", markdown.Code, markdown.Body.String())
125	}
126	branch := request("/team/nested/@/tree?ref=refs%2Fheads%2Ffeature%2Fwith-slash")
127	if branch.Code != http.StatusOK {
128		t.Errorf("slash branch status = %d: %s", branch.Code, branch.Body.String())
129	}
130	traversal := request("/team/nested/@/tree?ref=refs%2Fheads%2Fmain&path=..%2Fprivate")
131	if traversal.Code != http.StatusNotFound {
132		t.Errorf("traversal status = %d", traversal.Code)
133	}
134
135	png := request("/team/nested/@/raw?ref=refs%2Fheads%2Fmain&path=images%2Ficon.png")
136	if png.Code != http.StatusOK || png.Header().Get("Content-Type") != "image/png" || png.Header().Get("Content-Disposition") != "" || png.Header().Get("X-Content-Type-Options") != "nosniff" || png.Header().Get("Referrer-Policy") != "same-origin" {
137		t.Errorf("PNG response = %d, headers = %#v", png.Code, png.Header())
138	}
139	for _, name := range []string{"unsafe.svg", "unsafe.html", "unknown.xyz"} {
140		response := request("/team/nested/@/raw?ref=refs%2Fheads%2Fmain&path=" + name)
141		if response.Code != http.StatusOK || response.Header().Get("Content-Type") != "application/octet-stream" || !strings.HasPrefix(response.Header().Get("Content-Disposition"), "attachment;") {
142			t.Errorf("unsafe raw %s = %d, headers = %#v", name, response.Code, response.Header())
143		}
144	}
145}