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+"") 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, ``) cell := strings.Index(page, string(cgitTabs)) tabs := strings.Index(page, `>issues`) 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, ``); 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{ "": "alert", "": "onerror", "[click](javascript:alert(1))": "javascript:", "[click](data:text/html,%3Cs)": "data:", "hello": "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) } } }