Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Blame · server/home_test.go

Line attribution on main · 295 line(s)

074ae160
Keenan Nemetz
14d ago
1 package server_test
2
ab326d3a
Keenan Nemetz
14d ago
3 // The landing page is the instance's front door: identity, repositories, and a short
074ae160
Keenan Nemetz
14d ago
4 // activity rail. The full feed has its own page so the landing page can stay
5 // about the work rather than the log.
6
7 import (
20442b59
Keenan Nemetz
9d ago
8 "fmt"
074ae160
Keenan Nemetz
14d ago
9 "path/filepath"
10 "strings"
11 "testing"
20442b59
Keenan Nemetz
9d ago
12 "time"
074ae160
Keenan Nemetz
14d ago
13
d37d301f
Keenan Nemetz
16h ago
14 "rodent.software/vole/forge/config"
15 "rodent.software/vole/metadata"
074ae160
Keenan Nemetz
14d ago
16 )
17
18 func TestHomeIsALandingPage(t *testing.T) {
19 t.Parallel()
164ad9f5
Keenan Nemetz
1d ago
20 f := newFixtureWith(t, func(c *config.Config) {
21 c.InstanceName = "Acme Forge"
22 })
074ae160
Keenan Nemetz
14d ago
23 f.publish(f.repo)
6ce635a8
Keenan Nemetz
1d ago
24 f.instanceRepo(t, map[string][]byte{"profile/README.md": []byte("What this forge is for.")})
074ae160
Keenan Nemetz
14d ago
25
26 code, body := f.get("/")
27 if code != 200 {
28 t.Fatalf("home = %d", code)
29 }
30 for _, want := range []string{
164ad9f5
Keenan Nemetz
1d ago
31 `class="hero"`, "<h1>Acme Forge</h1>",
ff9870f1
Keenan Nemetz
1d ago
32 `readme-card`, `class="md readme-body"`, "What this forge is for.",
ed03b3d1
Keenan Nemetz
3h ago
33 "Repositories", `class="repo-card"`, `href="/demo"`,
34 "Latest", `href="/-/activity"`,
074ae160
Keenan Nemetz
14d ago
35 } {
36 if !strings.Contains(body, want) {
37 t.Fatalf("home is missing %q:\n%s", want, body)
38 }
39 }
ff9870f1
Keenan Nemetz
1d ago
40 for _, unwanted := range []string{
164ad9f5
Keenan Nemetz
1d ago
41 `class="readme-head"`, `class="readme-name"`,
ff9870f1
Keenan Nemetz
1d ago
42 `href="/?edit=1`, `name="intro"`,
43 } {
44 if strings.Contains(body, unwanted) {
164ad9f5
Keenan Nemetz
1d ago
45 t.Fatalf("home still has the old introduction header or editor %q:\n%s", unwanted, body)
ff9870f1
Keenan Nemetz
1d ago
46 }
47 }
074ae160
Keenan Nemetz
14d ago
48 if strings.Contains(body, "Recent activity") {
49 t.Fatalf("home still leads with the feed:\n%s", body)
50 }
ff9870f1
Keenan Nemetz
1d ago
51 if !strings.Contains(body, `<h2>Repositories</h2>`) {
52 t.Fatalf("home has no repository list heading:\n%s", body)
53 }
164ad9f5
Keenan Nemetz
1d ago
54 // The introduction belongs to the repository column, not the whole page.
55 main := strings.Index(body, `class="home-main"`)
56 readme := strings.Index(body, "readme-card")
57 rail := strings.Index(body, `class="home-rail"`)
58 if main < 0 || readme < 0 || rail < 0 || main > readme || readme > rail {
59 t.Fatalf("the introduction is not inside the repository column (main=%d readme=%d rail=%d):\n%s", main, readme, rail, body)
60 }
61 }
62
63 func TestHomeHeadingFallsBackToVole(t *testing.T) {
64 t.Parallel()
65 f := newFixture(t)
66 _, body := f.get("/")
67 if !strings.Contains(body, "<h1>Vole</h1>") {
68 t.Fatalf("home has no fallback heading:\n%s", body)
69 }
6ce635a8
Keenan Nemetz
1d ago
70 }
71
164ad9f5
Keenan Nemetz
1d ago
72 func TestHomePutsNewRepositoryInTheHero(t *testing.T) {
6ce635a8
Keenan Nemetz
1d ago
73 t.Parallel()
ff9870f1
Keenan Nemetz
1d ago
74 f := browserFixture(t)
75 f.publish(f.repo)
76 if _, err := f.srv.Identity.SetAdmin(f.ownerID, true, "test"); err != nil {
77 t.Fatal(err)
78 }
79 _, cookie := f.browserSession(t, f.ownerID)
80 _, body := f.browserGet(t, "/", cookie)
81
164ad9f5
Keenan Nemetz
1d ago
82 hero := strings.Index(body, `class="hero"`)
ed03b3d1
Keenan Nemetz
3h ago
83 button := strings.Index(body, `href="/-/repos/new"`)
164ad9f5
Keenan Nemetz
1d ago
84 grid := strings.Index(body, `class="home-grid"`)
85 if hero < 0 || button < 0 || grid < 0 || hero > button || button > grid {
86 t.Fatalf("the New repository action is not in the page header (hero=%d button=%d grid=%d):\n%s", hero, button, grid, body)
074ae160
Keenan Nemetz
14d ago
87 }
88 }
89
90 func TestHomeHasTheCleanTopBar(t *testing.T) {
91 t.Parallel()
92 f := newFixture(t)
93 f.publish(f.repo)
94
95 _, body := f.get("/")
96 for _, want := range []string{
ed03b3d1
Keenan Nemetz
3h ago
97 `class="topbar"`, `class="topnav"`, `href="/-/activity"`,
98 `class="topsearch"`, `class="topsearch-box"`, `href="/-/login"`,
074ae160
Keenan Nemetz
14d ago
99 } {
100 if !strings.Contains(body, want) {
101 t.Fatalf("home is missing the top-bar marker %q:\n%s", want, body)
102 }
103 }
ed03b3d1
Keenan Nemetz
3h ago
104 for _, unwanted := range []string{`class="rail"`, `src="/-/static/nav.js"`, `>Projects</a>`, `>Repositories</a>`, `href="/-/runners"`, `href="/-/admin"`} {
074ae160
Keenan Nemetz
14d ago
105 if strings.Contains(body, unwanted) {
106 t.Fatalf("home still has %q:\n%s", unwanted, body)
107 }
108 }
109
110 _, cookie := f.browserSession(t, f.ownerID)
111 _, body = f.browserGet(t, "/", cookie)
ed03b3d1
Keenan Nemetz
3h ago
112 for _, want := range []string{`class="account-menu"`, `class="account-pop"`, `href="/-/account"`, `action="/-/logout"`} {
074ae160
Keenan Nemetz
14d ago
113 if !strings.Contains(body, want) {
114 t.Fatalf("signed-in top bar is missing %q:\n%s", want, body)
115 }
116 }
ed03b3d1
Keenan Nemetz
3h ago
117 if strings.Contains(body, `href="/-/admin"`) {
4506a4d3
Keenan Nemetz
13d ago
118 t.Fatalf("non-admin sees the Status link:\n%s", body)
119 }
120
121 if _, err := f.srv.Identity.SetAdmin(f.ownerID, true, "test"); err != nil {
122 t.Fatal(err)
123 }
124 _, body = f.browserGet(t, "/", cookie)
ed03b3d1
Keenan Nemetz
3h ago
125 if !strings.Contains(body, `href="/-/admin"`) {
4506a4d3
Keenan Nemetz
13d ago
126 t.Fatalf("instance admin does not see the Status link:\n%s", body)
127 }
074ae160
Keenan Nemetz
14d ago
128 }
129
130 func TestHomeSearchShowsProjectCards(t *testing.T) {
131 t.Parallel()
132 f := newFixture(t)
133 f.publish(f.repo)
134
135 _, body := f.get("/?q=demo")
ed03b3d1
Keenan Nemetz
3h ago
136 for _, want := range []string{"Results", `class="repo-card"`, `href="/demo"`, "clear search"} {
074ae160
Keenan Nemetz
14d ago
137 if !strings.Contains(body, want) {
138 t.Fatalf("search results are missing %q:\n%s", want, body)
139 }
140 }
141 if strings.Contains(body, `class="home-rail"`) {
142 t.Fatalf("search results kept the landing-page activity rail:\n%s", body)
143 }
144
145 _, body = f.get("/?q=nomatch")
146 if !strings.Contains(body, "No repositories match") {
147 t.Fatalf("empty search state missing:\n%s", body)
148 }
149 }
150
151 func TestInstanceActivityListsOnlyVisibleRepositories(t *testing.T) {
152 t.Parallel()
153 f := newFixture(t)
154 f.publish(f.repo)
155
156 // The public repository has a commit (not an event), an issue, and a release.
157 work := workRepo(t, map[string]string{"readme.md": "public\n"})
158 if _, errs, err := runGit(t, work, "commit", "--allow-empty", "-qm", "public-commit"); err != nil {
159 t.Fatalf("public commit: %s", errs)
160 }
161 if _, errs, err := runGit(t, work, "tag", "-a", "v1.0.0", "-m", "public release"); err != nil {
162 t.Fatalf("public tag: %s", errs)
163 }
164 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main", "refs/tags/v1.0.0"); err != nil {
165 t.Fatalf("public push: %s", errs)
166 }
167 clone := filepath.Join(t.TempDir(), "clone")
168 if _, errs, err := f.git("", "clone", "-q", f.sshURL(f.repo), clone); err != nil {
169 t.Fatalf("public clone: %s", errs)
170 }
171 if _, errs, err := f.cli(clone, "issue", "new", "--title", "public-issue-marker", "-m", "body"); err != nil {
172 t.Fatalf("public issue: %v\n%s", err, errs)
173 }
174
175 secret, err := f.srv.Repos.Create("secret", f.ownerID, "private")
176 if err != nil {
177 t.Fatal(err)
178 }
179 if err := f.srv.Index.Upsert(secret); err != nil {
180 t.Fatal(err)
181 }
182 privateWork := workRepo(t, map[string]string{"hidden.md": "private\n"})
183 if _, errs, err := runGit(t, privateWork, "commit", "--allow-empty", "-qm", "private-commit"); err != nil {
184 t.Fatalf("private commit: %s", errs)
185 }
186 if _, errs, err := f.git(privateWork, "push", f.sshURL("secret"), "main:refs/heads/main"); err != nil {
187 t.Fatalf("private push: %s", errs)
188 }
189 privateClone := filepath.Join(t.TempDir(), "private-clone")
190 if _, errs, err := f.git("", "clone", "-q", f.sshURL("secret"), privateClone); err != nil {
191 t.Fatalf("private clone: %s", errs)
192 }
193 if _, errs, err := f.cli(privateClone, "issue", "new", "--title", "private-issue-marker", "-m", "body"); err != nil {
194 t.Fatalf("private issue: %v\n%s", err, errs)
195 }
196
ed03b3d1
Keenan Nemetz
3h ago
197 code, body := f.get("/-/activity")
074ae160
Keenan Nemetz
14d ago
198 if code != 200 {
199 t.Fatalf("activity = %d", code)
200 }
ed03b3d1
Keenan Nemetz
3h ago
201 for _, want := range []string{`href="/demo"`, "public-issue-marker", "v1.0.0", "released"} {
074ae160
Keenan Nemetz
14d ago
202 if !strings.Contains(body, want) {
203 t.Fatalf("activity feed is missing %q:\n%s", want, body)
204 }
205 }
ed03b3d1
Keenan Nemetz
3h ago
206 for _, unwanted := range []string{"private-issue-marker", `href="/secret"`, "public-commit", "private-commit", "committed"} {
074ae160
Keenan Nemetz
14d ago
207 if strings.Contains(body, unwanted) {
208 t.Fatalf("activity feed contains %q:\n%s", unwanted, body)
209 }
210 }
211
ed03b3d1
Keenan Nemetz
3h ago
212 _, body = f.get("/-/activity?kind=issues")
074ae160
Keenan Nemetz
14d ago
213 if !strings.Contains(body, "public-issue-marker") || strings.Contains(body, "v1.0.0") {
214 t.Fatalf("issue filter is wrong:\n%s", body)
215 }
ed03b3d1
Keenan Nemetz
3h ago
216 _, body = f.get("/-/activity?kind=releases")
074ae160
Keenan Nemetz
14d ago
217 if !strings.Contains(body, "v1.0.0") || strings.Contains(body, "public-issue-marker") {
218 t.Fatalf("release filter is wrong:\n%s", body)
219 }
220 }
221
222 func TestFirstRunOnboardingIsAnInstanceAdminPage(t *testing.T) {
223 t.Parallel()
224 f := newFixture(t)
225
226 // The fixture's one repository is owned by the account we sign in as, so
227 // remove it to reach the state a fresh instance is in: an admin and no
228 // repositories.
229 m, err := f.srv.Repos.Open(f.repo)
230 if err != nil {
231 t.Fatal(err)
232 }
b1499c72
Keenan Nemetz
12d ago
233 if err := f.srv.Repos.Delete(f.repo, f.ownerID); err != nil {
074ae160
Keenan Nemetz
14d ago
234 t.Fatal(err)
235 }
236 if err := f.srv.Index.Remove(m.ID); err != nil {
237 t.Fatal(err)
238 }
239 if _, err := f.srv.Identity.SetAdmin(f.ownerID, true, "test"); err != nil {
240 t.Fatal(err)
241 }
242 _, cookie := f.browserSession(t, f.ownerID)
243 resp, body := f.browserGet(t, "/", cookie)
244 if resp.StatusCode != 200 {
245 t.Fatalf("home = %d", resp.StatusCode)
246 }
ed03b3d1
Keenan Nemetz
3h ago
247 for _, want := range []string{"No repositories yet", "New repository", "Or from the instance directory", "--owner keenan", `href="/-/repos/new"`} {
074ae160
Keenan Nemetz
14d ago
248 if !strings.Contains(body, want) {
249 t.Fatalf("first-run page is missing %q:\n%s", want, body)
250 }
251 }
252 }
253
254 func TestHomeCardShowsTopLanguage(t *testing.T) {
255 t.Parallel()
256 f := newFixture(t)
257 f.publish(f.repo)
258 work := workRepo(t, map[string]string{"main.go": "package main\n", "README.md": "readme\n"})
259 if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil {
260 t.Fatalf("push: %s", errs)
261 }
262
263 _, body := f.get("/")
264 if !strings.Contains(body, `class="lang-dot lang-go"`) || !strings.Contains(body, `class="repo-lang"`) {
265 t.Fatalf("home card has no top-language marker:\n%s", body)
266 }
267 }
20442b59
Keenan Nemetz
9d ago
268
269 // TestHomeRailShowsTheNewestEventsAcrossRepositories pins the rail to the events
270 // table: the newest events, bounded to what fits, with no per-repository walk of
271 // the whole feed.
272 func TestHomeRailShowsTheNewestEventsAcrossRepositories(t *testing.T) {
273 t.Parallel()
274 f := newFixture(t)
275 f.publish(f.repo)
6ce635a8
Keenan Nemetz
1d ago
276 f.instanceRepo(t, map[string][]byte{"profile/README.md": []byte("What this forge is for.")})
20442b59
Keenan Nemetz
9d ago
277 m := mustRepo(t, f)
278 base := time.Date(2026, 10, 30, 12, 0, 0, 0, time.UTC)
279 for i := 0; i < 12; i++ {
280 iss, err := metadata.NewIssue(fmt.Sprintf("rail %02d", i), "body\n", "keenan", nil, nil, base.Add(time.Duration(i)*time.Minute))
281 if err != nil {
282 t.Fatal(err)
283 }
284 if err := metadata.CreateIssue(m.Git(), iss, "keenan"); err != nil {
285 t.Fatal(err)
286 }
287 }
288 _, body := f.get("/")
289 if n := strings.Count(body, `class="activity-event"`); n != 10 {
290 t.Fatalf("the rail shows %d events, want %d", n, 10)
291 }
292 if !strings.Contains(body, "rail 11") || strings.Contains(body, "rail 00") {
293 t.Fatalf("the rail is not the newest events:\n%s", body)
294 }
295 }