aboutsummaryrefslogtreecommitdiff
path: root/cgit_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'cgit_test.go')
-rw-r--r--cgit_test.go151
1 files changed, 151 insertions, 0 deletions
diff --git a/cgit_test.go b/cgit_test.go
new file mode 100644
index 0000000..1d62694
--- /dev/null
+++ b/cgit_test.go
@@ -0,0 +1,151 @@
+package main
+
+import (
+ "bytes"
+ "context"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "strings"
+ "testing"
+ "time"
+)
+
+// Every cgit page forks a child, and browsing a public repository takes no
+// credential, so the budget is what stands between a visitor and one child per
+// connection. A request that cannot have a slot waits for one, but not on
+// behalf of somebody who has already hung up.
+func TestBrowsingIsBounded(t *testing.T) {
+ f := testForge(t)
+ addRepo(t, f, "notes", Repo{Visibility: visPublic, Issues: featDisabled, Wiki: featDisabled})
+ h := f.routes()
+
+ for range cap(browsing) {
+ browsing <- struct{}{}
+ }
+ defer func() {
+ for range cap(browsing) {
+ <-browsing
+ }
+ }()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ cancel()
+ done := make(chan struct{})
+ go func() {
+ h.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/notes/", nil).WithContext(ctx))
+ close(done)
+ }()
+ select {
+ case <-done:
+ case <-time.After(5 * time.Second):
+ t.Fatal("a browse held a goroutine waiting to fork for a client that had gone")
+ }
+}
+
+// A tab that leads to a 404 is worse than no tab. One template draws the row
+// for both kinds of page, so both are asked here.
+func TestTabsFollowTheKnobs(t *testing.T) {
+ f := testForge(t)
+ addRepo(t, f, "docs", Repo{Visibility: visPublic, Issues: featDisabled, Wiki: featUsers})
+ addRepo(t, f, "code", Repo{Visibility: visPublic, Issues: featUsers, Wiki: featDisabled})
+ h := f.routes()
+
+ for _, c := range []struct {
+ path, page string
+ issues, wiki bool
+ }{
+ {"docs", "/docs/wiki", false, true},
+ {"code", "/code/issues", true, false},
+ } {
+ repo, _ := f.store.Repo(c.path)
+ r := httptest.NewRequest("GET", "/"+c.path+"/", nil)
+ rows := []struct{ where, html string }{
+ {"injected into cgit's row", string(f.navPart(r, "tabs", c.path, "", repo))},
+ {"on our own page", get(h, c.page, nil).Body.String()},
+ }
+ for _, row := range rows {
+ for _, tab := range []struct {
+ name string
+ want bool
+ }{{"issues", c.issues}, {"wiki", c.wiki}} {
+ got := strings.Contains(row.html, ">"+tab.name+"</a>")
+ if got != tab.want {
+ t.Errorf("%s %s: the %s tab is present %v, wanted %v",
+ c.path, row.where, tab.name, got, tab.want)
+ }
+ }
+ }
+ }
+}
+
+// The bar is spliced into cgit's own markup by matching it, so a cgit whose
+// page has changed shape has to fail here rather than quietly serve a page
+// with no bar on it.
+func TestTheBarGoesInsideCgitsPage(t *testing.T) {
+ f := testForge(t)
+ addRepo(t, f, "notes", Repo{Visibility: visPublic, Issues: featUsers, Wiki: featUsers})
+ if _, err := os.Stat(cgitBinary()); err != nil {
+ t.Skip("cgit is not installed, and this is a test of cgit's markup")
+ }
+ page := get(f.routes(), "/notes/", nil).Body.String()
+
+ div := strings.Index(page, string(cgitDiv))
+ header := strings.Index(page, `<table id="header">`)
+ cell := strings.Index(page, string(cgitTabs))
+ tabs := strings.Index(page, `>issues</a>`)
+ switch {
+ case div < 0:
+ t.Fatalf("cgit no longer opens its page with %s:\n%s", cgitDiv, page)
+ case cell < 0:
+ t.Fatalf("cgit no longer ends its tab cell with %s:\n%s", cgitTabs, page)
+ case header < 0:
+ t.Error("the header was not injected")
+ case header < div:
+ t.Error("the header landed outside cgit's own div")
+ case tabs < 0:
+ t.Error("the forge's tabs were not injected")
+ case tabs > cell:
+ t.Error("the forge's tabs landed after cgit's own tab cell rather than in it")
+ }
+ if n := strings.Count(page, `<table id="header">`); n != 1 {
+ t.Errorf("the header went in %d times", n)
+ }
+}
+
+// Only text/html is held back, and only until there is nothing left to wait
+// for: a snapshot has to stream through whole and untouched.
+func TestTheInjectorPassesThroughWhatItCannotPlace(t *testing.T) {
+ body := bytes.Repeat([]byte("x"), 3*maxInject)
+ for _, kind := range []string{"text/html; charset=utf-8", "application/x-gzip"} {
+ w := httptest.NewRecorder()
+ w.Header().Set("Content-Type", kind)
+ nav := &injector{ResponseWriter: w, nav: []byte("NAV"), tabs: []byte("TABS")}
+ nav.WriteHeader(http.StatusOK)
+ if _, err := nav.Write(body); err != nil {
+ t.Fatalf("%s: %v", kind, err)
+ }
+ nav.flush()
+ if got := w.Body.Bytes(); !bytes.Equal(got, body) {
+ t.Errorf("%s: %d bytes came back, of the %d that went in", kind, len(got), len(body))
+ }
+ }
+}
+
+// Bodies are handed to goldmark straight and the result is trusted as HTML, so
+// leaving raw HTML unrendered is the whole reason that is safe.
+func TestMarkdownIsNotHTML(t *testing.T) {
+ cases := map[string]string{
+ "<script>alert(1)</script>": "alert",
+ "<img src=x onerror=alert(1)>": "onerror",
+ "[click](javascript:alert(1))": "javascript:",
+ "[click](data:text/html,%3Cs)": "data:",
+ "<a href='vbscript:x'>hello</a>": "vbscript:",
+ }
+ for source, forbidden := range cases {
+ out := string(renderMarkdown(source))
+ if strings.Contains(out, forbidden) {
+ t.Errorf("%q rendered to %q, which still carries %q", source, out, forbidden)
+ }
+ }
+}