File view with blame information shown in the left gutter beside each line.

barerepo / server / e2e/crawl_test.go
176 lines · 6.5kb · master
log files threads runs releases config jump to file t
133728e barerepo 1mo
1
package e2e
133728e barerepo 1mo
2
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"html"
133728e barerepo 1mo
5
"net/http"
133728e barerepo 1mo
6
"net/url"
133728e barerepo 1mo
7
"os/exec"
133728e barerepo 1mo
8
"regexp"
133728e barerepo 1mo
9
"sort"
133728e barerepo 1mo
10
"strings"
133728e barerepo 1mo
11
"testing"
133728e barerepo 1mo
12
)
133728e barerepo 1mo
13
133728e barerepo 1mo
14
var hrefIn = regexp.MustCompile(`href="([^"]+)"`)
133728e barerepo 1mo
15
133728e barerepo 1mo
16
var saidIn = regexp.MustCompile(`<h2 class="sr-only">([^<]*)</h2>`)
133728e barerepo 1mo
17
133728e barerepo 1mo
18
// Nothing barerepo draws may lead nowhere, so every link on every page is followed and has to answer.
133728e barerepo 1mo
19
func TestNoPageLinksToSomethingThatDoesNotAnswer(t *testing.T) {
133728e barerepo 1mo
20
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
21
t.Skip("git is not installed")
133728e barerepo 1mo
22
}
133728e barerepo 1mo
23
in := newInstance(t)
133728e barerepo 1mo
24
john := in.account("john")
133728e barerepo 1mo
25
mark := in.account("mark")
133728e barerepo 1mo
26
work := seed(t, in, john, "john", "johnbot")
133728e barerepo 1mo
27
133728e barerepo 1mo
28
// A repository with something on every page, or the crawl walks past empty states.
133728e barerepo 1mo
29
write(t, work, "retry.go", "package main\n\nfunc backoff(n int) int { return n * 2 }\n")
133728e barerepo 1mo
30
// A space and a plus are legal in a path and are what break a url nobody escaped.
133728e barerepo 1mo
31
write(t, work, "a note.md", "# a file with a space in its name\n")
133728e barerepo 1mo
32
write(t, work, "c++.md", "# a file with a plus in its name\n")
133728e barerepo 1mo
33
// git quotes a path outside ascii in its own output, which is a second spelling to get wrong.
133728e barerepo 1mo
34
write(t, work, "café.md", "# a file with an accent in its name\n")
133728e barerepo 1mo
35
// A directory, so the tree has depth and an up link, and an awkward name inside it.
133728e barerepo 1mo
36
write(t, work, "docs/a note.md", "# a note in a directory\n")
133728e barerepo 1mo
37
// A binary and a large file, which are the two branches the file view draws instead of lines.
133728e barerepo 1mo
38
write(t, work, "logo.bin", "\x00\x01\x02binary\x00bytes\n")
133728e barerepo 1mo
39
write(t, work, "huge.txt", strings.Repeat("a line of a very large file\n", 40000))
133728e barerepo 1mo
40
write(t, work, "doomed.md", "# this file is deleted in the next commit\n")
133728e barerepo 1mo
41
run(t, work, "git", "add", "-A")
133728e barerepo 1mo
42
run(t, work, "git", "commit", "-qm", "add a backoff")
133728e barerepo 1mo
43
// A deleted file has no +++ b/ line, so its path comes from the other half of the header.
133728e barerepo 1mo
44
run(t, work, "git", "rm", "-q", "doomed.md")
133728e barerepo 1mo
45
run(t, work, "git", "commit", "-qm", "delete a file")
133728e barerepo 1mo
46
run(t, work, "git", "tag", "-a", "v1.0.0", "-m", "the first release")
133728e barerepo 1mo
47
// A slash is legal in a ref and it is what breaks a url that spends one path element on one.
133728e barerepo 1mo
48
run(t, work, "git", "tag", "-a", "release/1.0", "-m", "a tag with a slash in it")
133728e barerepo 1mo
49
run(t, work, "git", "branch", "feature/login")
133728e barerepo 1mo
50
run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "master")
133728e barerepo 1mo
51
run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "feature/login")
133728e barerepo 1mo
52
run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "v1.0.0")
133728e barerepo 1mo
53
run(t, work, "git", "push", "-q", in.url(john, "/john/johnbot"), "release/1.0")
133728e barerepo 1mo
54
133728e barerepo 1mo
55
mine := clone(t, in, mark, "/john/johnbot")
133728e barerepo 1mo
56
commit(t, mine, "package main\n\nfunc main() {}\n", "a proposal")
133728e barerepo 1mo
57
run(t, mine, "git", "push", "-q", in.url(mark, "/john/johnbot"), "HEAD:refs/proposals/new")
133728e barerepo 1mo
58
133728e barerepo 1mo
59
post(t, in, "john", "/john/johnbot/threads", url.Values{"title": {"a thread"}, "body": {"with a body"}})
133728e barerepo 1mo
60
133728e barerepo 1mo
61
// A page has as many versions as it has kinds of reader, and each draws its own links.
133728e barerepo 1mo
62
owner, err := in.db.NewSession(t.Context(), "john")
133728e barerepo 1mo
63
if err != nil {
133728e barerepo 1mo
64
t.Fatal(err)
133728e barerepo 1mo
65
}
133728e barerepo 1mo
66
stranger, err := in.db.NewSession(t.Context(), "mark")
133728e barerepo 1mo
67
if err != nil {
133728e barerepo 1mo
68
t.Fatal(err)
133728e barerepo 1mo
69
}
133728e barerepo 1mo
70
for _, who := range []struct {
133728e barerepo 1mo
71
name string
133728e barerepo 1mo
72
session string
133728e barerepo 1mo
73
least int
133728e barerepo 1mo
74
}{{"the owner", owner, 25}, {"a stranger", stranger, 15}, {"nobody", "", 10}} {
133728e barerepo 1mo
75
crawl(t, in, who.name, who.session, who.least, who.name == "the owner")
133728e barerepo 1mo
76
}
133728e barerepo 1mo
77
}
133728e barerepo 1mo
78
133728e barerepo 1mo
79
// crawl follows every link one reader is shown, because a link that refuses them is not a link.
133728e barerepo 1mo
80
func crawl(t *testing.T, in *instance, who, session string, least int, isOwner bool) {
133728e barerepo 1mo
81
t.Helper()
133728e barerepo 1mo
82
queue := []string{"/", "/john", "/john/johnbot", "/keys", "/inbox", "/search?q=backoff", "/signup"}
133728e barerepo 1mo
83
// A seed is typed, so being sent to sign in is a fair answer. A drawn link is a promise.
133728e barerepo 1mo
84
seed := map[string]bool{}
133728e barerepo 1mo
85
for _, p := range queue {
133728e barerepo 1mo
86
seed[p] = true
133728e barerepo 1mo
87
}
133728e barerepo 1mo
88
seen := map[string]bool{}
133728e barerepo 1mo
89
var broken []string
133728e barerepo 1mo
90
for len(queue) > 0 && len(seen) < 300 {
133728e barerepo 1mo
91
path := queue[0]
133728e barerepo 1mo
92
queue = queue[1:]
133728e barerepo 1mo
93
if seen[path] {
133728e barerepo 1mo
94
continue
133728e barerepo 1mo
95
}
133728e barerepo 1mo
96
seen[path] = true
133728e barerepo 1mo
97
133728e barerepo 1mo
98
code, where, body := fetch(t, in, session, path)
133728e barerepo 1mo
99
if code != http.StatusOK && code != http.StatusFound {
133728e barerepo 1mo
100
broken = append(broken, path+" answered "+itoaCode(code))
133728e barerepo 1mo
101
continue
133728e barerepo 1mo
102
}
133728e barerepo 1mo
103
// A link that can only send this reader to the sign-in page is a link they should not see.
133728e barerepo 1mo
104
if !seed[path] && strings.HasPrefix(where, "/signin") {
133728e barerepo 1mo
105
broken = append(broken, path+", which only sends them to sign in")
133728e barerepo 1mo
106
continue
133728e barerepo 1mo
107
}
133728e barerepo 1mo
108
// Every mock carries one sr-only sentence saying what its page is for, and so must every page.
133728e barerepo 1mo
109
if strings.HasPrefix(body, "<!doctype html>") {
133728e barerepo 1mo
110
said := saidIn.FindStringSubmatch(body)
133728e barerepo 1mo
111
if said == nil || strings.TrimSpace(said[1]) == "" {
133728e barerepo 1mo
112
broken = append(broken, path+" says nothing about itself to a screen reader")
133728e barerepo 1mo
113
}
133728e barerepo 1mo
114
}
133728e barerepo 1mo
115
for _, m := range hrefIn.FindAllStringSubmatch(body, -1) {
133728e barerepo 1mo
116
if next, ok := internal(m[1]); ok {
133728e barerepo 1mo
117
queue = append(queue, next)
133728e barerepo 1mo
118
}
133728e barerepo 1mo
119
}
133728e barerepo 1mo
120
}
133728e barerepo 1mo
121
if len(seen) < least {
133728e barerepo 1mo
122
t.Fatalf("%s reached only %d pages, so the crawl is not reading links", who, len(seen))
133728e barerepo 1mo
123
}
133728e barerepo 1mo
124
// A page nothing links to is a page nobody finds, whatever it answers when asked directly.
133728e barerepo 1mo
125
if isOwner {
133728e barerepo 1mo
126
for _, must := range []string{
133728e barerepo 1mo
127
"/john/johnbot", "/john/johnbot/files", "/john/johnbot/threads",
133728e barerepo 1mo
128
"/john/johnbot/runs", "/john/johnbot/releases", "/john/johnbot/config",
133728e barerepo 1mo
129
"/john/johnbot/thread/2", "/keys", "/inbox",
133728e barerepo 1mo
130
} {
133728e barerepo 1mo
131
if !seen[must] {
133728e barerepo 1mo
132
t.Errorf("nothing links to %s, so a reader can only reach it by typing", must)
133728e barerepo 1mo
133
}
133728e barerepo 1mo
134
}
133728e barerepo 1mo
135
}
133728e barerepo 1mo
136
sort.Strings(broken)
133728e barerepo 1mo
137
for _, b := range broken {
133728e barerepo 1mo
138
t.Errorf("%s is shown a link to %s", who, b)
133728e barerepo 1mo
139
}
133728e barerepo 1mo
140
}
133728e barerepo 1mo
141
133728e barerepo 1mo
142
// internal keeps the links that stay on this barerepo, since an outside url is not barerepo's to answer.
133728e barerepo 1mo
143
func internal(href string) (string, bool) {
133728e barerepo 1mo
144
if !strings.HasPrefix(href, "/") || strings.HasPrefix(href, "//") {
133728e barerepo 1mo
145
return "", false
133728e barerepo 1mo
146
}
133728e barerepo 1mo
147
if strings.HasPrefix(href, "/static/") {
133728e barerepo 1mo
148
return "", false
133728e barerepo 1mo
149
}
133728e barerepo 1mo
150
// A page writes & as &amp; and + as &#43;, and a crawler must read what a browser would.
133728e barerepo 1mo
151
return html.UnescapeString(href), true
133728e barerepo 1mo
152
}
133728e barerepo 1mo
153
133728e barerepo 1mo
154
func fetch(t *testing.T, in *instance, session, path string) (int, string, string) {
133728e barerepo 1mo
155
t.Helper()
133728e barerepo 1mo
156
req, err := http.NewRequest(http.MethodGet, in.http.URL+path, nil)
133728e barerepo 1mo
157
if err != nil {
133728e barerepo 1mo
158
t.Fatal(err)
133728e barerepo 1mo
159
}
133728e barerepo 1mo
160
if session != "" {
133728e barerepo 1mo
161
req.AddCookie(&http.Cookie{Name: "barerepo_session", Value: session})
133728e barerepo 1mo
162
}
133728e barerepo 1mo
163
client := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error {
133728e barerepo 1mo
164
return http.ErrUseLastResponse
133728e barerepo 1mo
165
}}
133728e barerepo 1mo
166
resp, err := client.Do(req)
133728e barerepo 1mo
167
if err != nil {
133728e barerepo 1mo
168
t.Fatal(err)
133728e barerepo 1mo
169
}
133728e barerepo 1mo
170
defer resp.Body.Close()
133728e barerepo 1mo
171
return resp.StatusCode, resp.Header.Get("Location"), readAll(t, resp)
133728e barerepo 1mo
172
}
133728e barerepo 1mo
173
133728e barerepo 1mo
174
func itoaCode(n int) string {
133728e barerepo 1mo
175
return string(rune('0'+n/100)) + string(rune('0'+n/10%10)) + string(rune('0'+n%10))
133728e barerepo 1mo
176
}
history · rawbarerepo 0.1.0