aboutsummaryrefslogtreecommitdiff
path: root/cgit_test.go
blob: 1d626945f1d22e3efc37fe42ba4c342e303e53d4 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
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)
		}
	}
}