e2e/design_test.go
215 lines · 9272 bytes
1package e2e
2
3import (
4 "encoding/json"
5 "fmt"
6 "net/http"
7 "net/url"
8 "os"
9 "path/filepath"
10 "strings"
11 "testing"
12)
13
14func TestReadmeRelativeLinks(t *testing.T) {
15 inst := startInstance(t)
16 aliceKey := inst.newKey(t, "alice")
17 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
18 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/site"); code != 0 {
19 t.Fatal("repo create failed")
20 }
21 work := t.TempDir()
22 env := inst.gitEnv(aliceKey)
23 mustGit(t, work, env, "clone", inst.sshURL("alice/site"), "w")
24 dir := filepath.Join(work, "w")
25 os.MkdirAll(filepath.Join(dir, "docs"), 0o755)
26 os.MkdirAll(filepath.Join(dir, "img"), 0o755)
27 os.WriteFile(filepath.Join(dir, "README.md"), []byte(
28 "# site\n\n[guide](docs/guide.md) and [export](docs/paper.html) and "+
29 "[abs](https://example.org/x) here\n\n\n"+
30 "\n\n"+
31 "| flag | effect |\n|------|--------|\n| `-v` | verbose |\n"), 0o644)
32 os.WriteFile(filepath.Join(dir, "docs", "guide.md"), []byte("# guide\n"), 0o644)
33 os.WriteFile(filepath.Join(dir, "docs", "paper.org"), []byte("* paper\n"), 0o644)
34 os.WriteFile(filepath.Join(dir, "img", "logo.png"), []byte{0x89, 0x50}, 0o644)
35 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
36 mustGit(t, dir, env, "add", ".")
37 mustGit(t, dir, env, "commit", "-q", "-m", "base")
38 mustGit(t, dir, env, "push", "-q", "origin", "main")
39
40 status, body := inst.get(t, "/alice/site")
41 if status != 200 {
42 t.Fatalf("tree: %d", status)
43 }
44 for _, want := range []string{
45 `href="/alice/site/blob/main/docs/guide.md"`, // relative link
46 `href="/alice/site/blob/main/docs/paper.org"`, // .html mapped to .org source
47 `src="/alice/site/raw/main/img/logo.png"`, // relative image via raw
48 `src="https://example.org/pic.png"`, // remote image untouched
49 `href="https://example.org/x"`, // absolute untouched
50 "<table>", "<td>verbose</td>", // GFM table renders
51 `href="/alice/site/blob/main/README.md">README.md</a>`, // clickable card header
52 `<th>name</th>`, // file table column headers
53 } {
54 if !strings.Contains(body, want) {
55 t.Errorf("missing %q", want)
56 }
57 }
58 // Branch dropdown lists branches.
59 if !strings.Contains(body, `class="refmenu"`) || !strings.Contains(body, ">all refs") {
60 t.Error("branch dropdown missing")
61 }
62 // Per-file history: ?path= filters the log; blob pages link to it.
63 os.WriteFile(filepath.Join(dir, "docs", "notes.txt"), []byte("n\n"), 0o644)
64 mustGit(t, dir, env, "add", ".")
65 mustGit(t, dir, env, "commit", "-q", "-m", "touch only the notes")
66 mustGit(t, dir, env, "push", "-q", "origin", "main")
67 if _, body := inst.get(t, "/alice/site/log?path=docs/notes.txt"); !strings.Contains(body, "touch only the notes") ||
68 strings.Contains(body, ">base<") || !strings.Contains(body, "history of") {
69 t.Fatalf("per-file log wrong:\n%s", body)
70 }
71 if _, body := inst.get(t, "/alice/site/log?path=no/such/file"); !strings.Contains(body, "nothing touches") {
72 t.Fatalf("empty per-file log:\n%s", body)
73 }
74 if _, body := inst.get(t, "/alice/site/blob/main/docs/guide.md"); !strings.Contains(body, `log/main?path=docs%2fguide.md">history</a>`) {
75 t.Fatalf("blob history link missing:\n%s", body)
76 }
77 // CLI parity: repo log --path.
78 logOut, _, _ := inst.ssh(t, aliceKey, "", "repo", "log", "alice/site", "--path", "docs/notes.txt", "--json")
79 if !strings.Contains(logOut, "touch only the notes") || strings.Contains(logOut, `"subject":"base"`) {
80 t.Fatalf("repo log --path wrong:\n%s", logOut)
81 }
82 // Raw serves images with their real type (nosniff otherwise blocks
83 // <img>); everything else stays inert text/plain.
84 resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d/alice/site/raw/main/img/logo.png", inst.httpPort))
85 if err != nil {
86 t.Fatal(err)
87 }
88 resp.Body.Close()
89 if ct := resp.Header.Get("Content-Type"); ct != "image/png" {
90 t.Errorf("raw png content-type = %q", ct)
91 }
92 resp, err = http.Get(fmt.Sprintf("http://127.0.0.1:%d/alice/site/raw/main/README.md", inst.httpPort))
93 if err != nil {
94 t.Fatal(err)
95 }
96 resp.Body.Close()
97 if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") {
98 t.Errorf("raw md content-type = %q", ct)
99 }
100 // Blob pages preview images inline.
101 if _, body := inst.get(t, "/alice/site/blob/main/img/logo.png"); !strings.Contains(body, `<img src="/alice/site/raw/main/img/logo.png"`) {
102 t.Errorf("blob image preview missing:\n%s", body)
103 }
104 // Explore rows carry topics, license, and updated date.
105 inst.ssh(t, aliceKey, "", "repo", "topics", "add", "alice/site", "web")
106 // Bare 0BSD grant (no notice-retention clause), wrapped mid-sentence.
107 os.WriteFile(filepath.Join(dir, "LICENSE"), []byte("Permission to use, copy, modify,\nand/or distribute this software for any\npurpose with or without fee is hereby granted.\n"), 0o644)
108 mustGit(t, dir, env, "add", ".")
109 mustGit(t, dir, env, "commit", "-q", "-m", "license")
110 mustGit(t, dir, env, "push", "-q", "origin", "main")
111 _, body = inst.get(t, "/explore")
112 for _, want := range []string{`href="/explore?q=web"`, "0BSD", "updated 20"} {
113 if !strings.Contains(body, want) {
114 t.Errorf("explore row missing %q", want)
115 }
116 }
117}
118
119func TestWebInteractions(t *testing.T) {
120 inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
121 aliceKey := inst.newKey(t, "alice")
122 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
123 if _, _, code := inst.ssh(t, aliceKey, "", "org", "create", "theorg"); code != 0 {
124 t.Fatal("org create failed")
125 }
126
127 out, _, code := inst.ssh(t, aliceKey, "", "web", "login", "--json")
128 if code != 0 {
129 t.Fatal("web login failed")
130 }
131 var env2 struct {
132 Data struct {
133 URL string `json:"url"`
134 } `json:"data"`
135 }
136 json.Unmarshal([]byte(out), &env2)
137 browser := newBrowser(t)
138 browserGet(t, browser, inst.base()+env2.Data.URL[strings.Index(env2.Data.URL, "/login"):])
139
140 // Create a repo under the org through the web form.
141 if status, _ := browserPost(t, browser, inst.base()+"/new",
142 url.Values{"owner": {"theorg"}, "name": {"webborn"}, "visibility": {"public"}}); status != 200 {
143 t.Fatalf("org repo via web: %d", status)
144 }
145 if out, _, code := inst.ssh(t, aliceKey, "", "repo", "show", "theorg/webborn"); code != 0 {
146 t.Fatalf("org repo missing: %s", out)
147 }
148
149 // Pin from the web; the repo header reflects it and the dashboard
150 // lists it; a second toggle unpins.
151 if status, _ := browserPost(t, browser, inst.base()+"/theorg/webborn/pin", url.Values{}); status != 200 {
152 t.Fatal("pin toggle failed")
153 }
154 _, body := browserGet(t, browser, inst.base()+"/theorg/webborn")
155 if !strings.Contains(body, "★ pinned") {
156 t.Fatal("repo header not pinned")
157 }
158 if _, body = browserGet(t, browser, inst.base()+"/"); !strings.Contains(body, "theorg<span") {
159 t.Fatal("dashboard missing pinned repo")
160 }
161 browserPost(t, browser, inst.base()+"/theorg/webborn/pin", url.Values{})
162 if _, body = browserGet(t, browser, inst.base()+"/theorg/webborn"); !strings.Contains(body, "☆ pin") {
163 t.Fatal("unpin failed")
164 }
165
166 // New issue with labels through the form; label chips filter.
167 if status, _ := browserPost(t, browser, inst.base()+"/theorg/webborn/issues/new",
168 url.Values{"title": {"styled"}, "body": {"b"}, "labels": {"bug ui"}}); status != 200 {
169 t.Fatal("issue via web failed")
170 }
171 if status, _ := browserPost(t, browser, inst.base()+"/theorg/webborn/issues/new",
172 url.Values{"title": {"plain"}}); status != 200 {
173 t.Fatal("second issue failed")
174 }
175 _, body = browserGet(t, browser, inst.base()+"/theorg/webborn/issues?label=bug")
176 if !strings.Contains(body, "styled") || strings.Contains(body, ">plain<") {
177 t.Fatalf("label filter wrong:\n%s", body)
178 }
179 _, body = browserGet(t, browser, inst.base()+"/theorg/webborn/issues/1")
180 if !strings.Contains(body, `href="/theorg/webborn/issues?label=bug"`) ||
181 !strings.Contains(body, `href="/alice">alice</a>`) {
182 t.Fatal("issue page chips/author not linked")
183 }
184}
185
186func TestCommitParentLinks(t *testing.T) {
187 inst := startInstance(t)
188 aliceKey := inst.newKey(t, "alice")
189 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
190 if _, _, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
191 t.Fatal("repo create failed")
192 }
193 work := t.TempDir()
194 env := inst.gitEnv(aliceKey)
195 mustGit(t, work, env, "clone", inst.sshURL("alice/app"), "w")
196 dir := filepath.Join(work, "w")
197 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a\n"), 0o644)
198 mustGit(t, dir, env, "checkout", "-q", "-b", "main")
199 mustGit(t, dir, env, "add", ".")
200 mustGit(t, dir, env, "commit", "-q", "-m", "first")
201 first := strings.TrimSpace(mustGit(t, dir, env, "rev-parse", "HEAD"))
202 os.WriteFile(filepath.Join(dir, "a.txt"), []byte("b\n"), 0o644)
203 mustGit(t, dir, env, "commit", "-qam", "second")
204 head := strings.TrimSpace(mustGit(t, dir, env, "rev-parse", "HEAD"))
205 mustGit(t, dir, env, "push", "-q", "origin", "main")
206
207 _, body := inst.get(t, "/alice/app/commit/"+head)
208 if !strings.Contains(body, `href="/alice/app/commit/`+first+`"`) {
209 t.Fatal("parent commit not linked")
210 }
211 _, body = inst.get(t, "/alice/app/commit/"+first)
212 if strings.Contains(body, ">parent") {
213 t.Fatal("root commit shows a parent")
214 }
215}