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.

Serve repositories from the root of the URL space

ed03b3d1fa16ef6b55e2773aaf10e3be13282c3a · Keenan Nemetz · 4h ago · parent d37d301f

Serve repositories from the root of the URL space

A repository is now /<name> and its Git transport /<name>.git, so the URL a
person opens, the URL a clone uses, and the Go module path are the same name.
The forge's own pages moved under /-/ (admin, accounts, the directory, the
feed, sign-in, assets), a prefix no repository name can start with: names are
[a-z0-9...] and cannot begin with "-". That leaves the router with two disjoint
spaces and removes the /r/ and /git/ prefixes, which existed only to keep
repositories away from the forge's own paths.

Routes: repoRoutes lose /r/; global, auth and admin routes gain /-/; assets
move to /-/static/ because a subtree pattern like /static/ cannot coexist with
one-segment-wildcard repo routes — Go's mux reports them as conflicting
neither-more-specific patterns. The catch-all still renders the styled 404.

Transport: githttp.Route parses /<name>.git/<rest> instead of a /git/ prefix,
and forgepaths.go hands such requests to the proxy before the mux, because a
wildcard fills a whole segment and cannot match "<name>.git". LFS stays on the
mux (<name>.git/info/lfs/...), and the Go module discovery probe moves to the
same pre-router so a package path under a public repository is answered even
when its first segments name a page tab.

Names: repo.ValidateName now refuses the four root paths the forge must keep —
static, robots.txt, sitemap.xml, sitemap-pages.xml. Everything else is an
ordinary repository name, including admin, u, and activity.

Docs and the packaged URLs follow the new layout; robots.txt, the sitemaps and
the instance activity feed point at the new paths. No aliases: the old /r/ and
/git/ URLs are gone.

Verified with ./vole workflow check and ./vole workflow run verify.

server/auth_test.go +59−59

@@ -109,11 +109,11 @@ func TestBrowserLoginAndInvitePages(t *testing.T) {
109 109 t.Parallel()
110 110 f := browserFixture(t)
111 111
112 resp, body := f.browserGet(t, "/login?next=/r/demo", "")
112 resp, body := f.browserGet(t, "/-/login?next=/demo", "")
113 113 if resp.StatusCode != http.StatusOK || !strings.Contains(body, "Use a passkey") {
114 114 t.Fatalf("login page: status=%d body=%s", resp.StatusCode, body)
115 115 }
116 if !strings.Contains(body, `data-next="/r/demo"`) {
116 if !strings.Contains(body, `data-next="/demo"`) {
117 117 t.Fatalf("login page lost the next path: %s", body)
118 118 }
119 119
@@ -124,7 +124,7 @@ func TestBrowserLoginAndInvitePages(t *testing.T) {
124 124 // The token is in the fragment, so the server sees only /invite: the page is
125 125 // identical for a valid link and a stale one, and nothing about the token
126 126 // reaches a log. InviteBegin is what answers whether it is still good.
127 resp, body = f.browserGet(t, "/invite", "")
127 resp, body = f.browserGet(t, "/-/invite", "")
128 128 if resp.StatusCode != http.StatusOK || !strings.Contains(body, "Create a passkey") {
129 129 t.Fatalf("invite page: status=%d body=%s", resp.StatusCode, body)
130 130 }
@@ -135,8 +135,8 @@ func TestBrowserLoginAndInvitePages(t *testing.T) {
135 135 t.Fatalf("invite page referrer policy = %q", got)
136 136 }
137 137 // A link in the old shape still works: it is redirected into the fragment.
138 resp, _ = f.browserGet(t, "/invite/"+inv.Token, "")
139 if resp.StatusCode != http.StatusSeeOther || resp.Header.Get("Location") != "/invite#"+inv.Token {
138 resp, _ = f.browserGet(t, "/-/invite/"+inv.Token, "")
139 if resp.StatusCode != http.StatusSeeOther || resp.Header.Get("Location") != "/-/invite#"+inv.Token {
140 140 t.Fatalf("legacy invite link = %d %q", resp.StatusCode, resp.Header.Get("Location"))
141 141 }
142 142 }
@@ -144,11 +144,11 @@ func TestBrowserLoginAndInvitePages(t *testing.T) {
144 144 func TestLoginIsDisabledWithoutHTTPS(t *testing.T) {
145 145 t.Parallel()
146 146 f := newFixture(t)
147 resp, body := f.browserGet(t, "/login", "")
147 resp, body := f.browserGet(t, "/-/login", "")
148 148 if resp.StatusCode != http.StatusOK || !strings.Contains(body, "public_url") {
149 149 t.Fatalf("disabled login page: status=%d body=%s", resp.StatusCode, body)
150 150 }
151 resp, body = f.browserGet(t, "/invite", "")
151 resp, body = f.browserGet(t, "/-/invite", "")
152 152 if resp.StatusCode != http.StatusOK || !strings.Contains(body, "public_url") {
153 153 t.Fatalf("disabled invite page: status=%d body=%s", resp.StatusCode, body)
154 154 }
@@ -160,21 +160,21 @@ func TestSessionVisibilityAndLogoutCSRF(t *testing.T) {
160 160 sess, cookie := f.browserSession(t, f.ownerID)
161 161
162 162 // Anonymous still gets a private repository's 404.
163 resp, _ := f.browserGet(t, "/r/"+f.repo, "")
163 resp, _ := f.browserGet(t, "/"+f.repo, "")
164 164 if resp.StatusCode != http.StatusNotFound {
165 165 t.Fatalf("anonymous private repo status = %d, want 404", resp.StatusCode)
166 166 }
167 resp, body := f.browserGet(t, "/r/"+f.repo, cookie)
167 resp, body := f.browserGet(t, "/"+f.repo, cookie)
168 168 if resp.StatusCode != http.StatusOK || !strings.Contains(body, "keenan") || !strings.Contains(body, "account") {
169 169 t.Fatalf("logged-in repo page: status=%d body=%s", resp.StatusCode, body)
170 170 }
171 171
172 172 // Logout is a state-changing POST and needs the session's CSRF token.
173 resp, _ = f.browserPost(t, "/logout", cookie, url.Values{})
173 resp, _ = f.browserPost(t, "/-/logout", cookie, url.Values{})
174 174 if resp.StatusCode != http.StatusForbidden {
175 175 t.Fatalf("logout without CSRF status = %d, want 403", resp.StatusCode)
176 176 }
177 resp, _ = f.browserPost(t, "/logout", cookie, url.Values{"csrf": {sess.CSRF}})
177 resp, _ = f.browserPost(t, "/-/logout", cookie, url.Values{"csrf": {sess.CSRF}})
178 178 if resp.StatusCode != http.StatusSeeOther {
179 179 t.Fatalf("logout status = %d, want 303", resp.StatusCode)
180 180 }
@@ -188,17 +188,17 @@ func TestWebIssueAndCommentWritesUseSessionIdentity(t *testing.T) {
188 188 f := browserFixture(t)
189 189 sess, cookie := f.browserSession(t, f.ownerID)
190 190
191 resp, body := f.browserPost(t, "/r/"+f.repo+"/issues", cookie, url.Values{
191 resp, body := f.browserPost(t, "/"+f.repo+"/issues", cookie, url.Values{
192 192 "csrf": {sess.CSRF}, "title": {"Browser filed issue"}, "body": {"Written from the web."},
193 193 })
194 194 if resp.StatusCode != http.StatusSeeOther {
195 195 t.Fatalf("create issue: status=%d body=%s", resp.StatusCode, body)
196 196 }
197 197 loc := resp.Header.Get("Location")
198 if !strings.HasPrefix(loc, "/r/"+f.repo+"/issues/") {
198 if !strings.HasPrefix(loc, "/"+f.repo+"/issues/") {
199 199 t.Fatalf("create issue redirect = %q", loc)
200 200 }
201 id := strings.TrimPrefix(loc, "/r/"+f.repo+"/issues/")
201 id := strings.TrimPrefix(loc, "/"+f.repo+"/issues/")
202 202
203 203 resp, body = f.browserGet(t, loc, cookie)
204 204 if resp.StatusCode != http.StatusOK || !strings.Contains(body, "Browser filed issue") || !strings.Contains(body, "Written from the web.") {
@@ -225,7 +225,7 @@ func TestWebIssueAndCommentWritesUseSessionIdentity(t *testing.T) {
225 225 }
226 226
227 227 // A write without the token is refused before anything is committed.
228 resp, _ = f.browserPost(t, "/r/"+f.repo+"/issues", cookie, url.Values{
228 resp, _ = f.browserPost(t, "/"+f.repo+"/issues", cookie, url.Values{
229 229 "title": {"forged"},
230 230 })
231 231 if resp.StatusCode != http.StatusForbidden {
@@ -248,7 +248,7 @@ func TestWebApprovalNamesTheSessionAccount(t *testing.T) {
248 248 }
249 249
250 250 sess, cookie := f.browserSession(t, f.ownerID)
251 resp, body := f.browserPost(t, "/r/"+f.repo+"/changes/"+req.ID+"/approve", cookie,
251 resp, body := f.browserPost(t, "/"+f.repo+"/changes/"+req.ID+"/approve", cookie,
252 252 url.Values{"csrf": {sess.CSRF}, "base": {"main"}, "head": {head}})
253 253 if resp.StatusCode != http.StatusSeeOther {
254 254 t.Fatalf("approve: status=%d body=%s", resp.StatusCode, body)
@@ -282,7 +282,7 @@ func TestWebApprovalRefusesAStaleTarget(t *testing.T) {
282 282 }
283 283
284 284 sess, cookie := f.browserSession(t, f.ownerID)
285 resp, body := f.browserPost(t, "/r/"+f.repo+"/changes/"+req.ID+"/approve", cookie,
285 resp, body := f.browserPost(t, "/"+f.repo+"/changes/"+req.ID+"/approve", cookie,
286 286 url.Values{"csrf": {sess.CSRF}, "base": {"main"}, "head": {stale}})
287 287 if resp.StatusCode != http.StatusBadRequest {
288 288 t.Fatalf("stale approval: status=%d body=%s", resp.StatusCode, body)
@@ -296,7 +296,7 @@ func TestWebApprovalRefusesAStaleTarget(t *testing.T) {
296 296 if _, err := metadata.SetRequestBase(m.Git(), req.ID, "other", f.ownerID); err != nil {
297 297 t.Fatal(err)
298 298 }
299 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+req.ID+"/approve", cookie,
299 resp, body = f.browserPost(t, "/"+f.repo+"/changes/"+req.ID+"/approve", cookie,
300 300 url.Values{"csrf": {sess.CSRF}, "base": {"main"}, "head": {head}})
301 301 if resp.StatusCode != http.StatusBadRequest {
302 302 t.Fatalf("stale base approval: status=%d body=%s", resp.StatusCode, body)
@@ -328,7 +328,7 @@ func TestWebAuthnBeginEndpoints(t *testing.T) {
328 328 if err := f.srv.Identity.AddCredential(f.ownerID, identity.Credential{ID: id, PublicKey: pub, Label: "test"}, "test"); err != nil {
329 329 t.Fatal(err)
330 330 }
331 resp, out := f.browserJSON(t, "/auth/login/begin", "", "", map[string]string{"username": "keenan"})
331 resp, out := f.browserJSON(t, "/-/auth/login/begin", "", "", map[string]string{"username": "keenan"})
332 332 if resp.StatusCode != http.StatusOK || out["publicKey"] == nil {
333 333 t.Fatalf("login begin: status=%d body=%v", resp.StatusCode, out)
334 334 }
@@ -337,18 +337,18 @@ func TestWebAuthnBeginEndpoints(t *testing.T) {
337 337 if err != nil {
338 338 t.Fatal(err)
339 339 }
340 resp, out = f.browserJSON(t, "/auth/invite/begin", "", "", map[string]string{"token": inv.Token})
340 resp, out = f.browserJSON(t, "/-/auth/invite/begin", "", "", map[string]string{"token": inv.Token})
341 341 if resp.StatusCode != http.StatusOK || out["publicKey"] == nil {
342 342 t.Fatalf("invite begin: status=%d body=%v", resp.StatusCode, out)
343 343 }
344 344
345 345 // Account registration is state-changing and must demand the CSRF token.
346 346 sess, cookie := f.browserSession(t, f.ownerID)
347 resp, _ = f.browserJSON(t, "/account/passkeys/begin", cookie, "", map[string]string{"label": "phone"})
347 resp, _ = f.browserJSON(t, "/-/account/passkeys/begin", cookie, "", map[string]string{"label": "phone"})
348 348 if resp.StatusCode != http.StatusForbidden {
349 349 t.Fatalf("passkey begin without CSRF status = %d, want 403", resp.StatusCode)
350 350 }
351 resp, out = f.browserJSON(t, "/account/passkeys/begin", cookie, sess.CSRF, map[string]string{"label": "phone"})
351 resp, out = f.browserJSON(t, "/-/account/passkeys/begin", cookie, sess.CSRF, map[string]string{"label": "phone"})
352 352 if resp.StatusCode != http.StatusOK || out["publicKey"] == nil {
353 353 t.Fatalf("passkey begin: status=%d body=%v", resp.StatusCode, out)
354 354 }
@@ -394,19 +394,19 @@ func TestContributorNamespaceAndRequestUseTheAccountName(t *testing.T) {
394 394
395 395 sess, cookie := f.browserSession(t, contrib.Name)
396 396 // Contributors can discuss without access to shared branches.
397 resp, body := f.browserPost(t, "/r/"+f.repo+"/issues", cookie, url.Values{
397 resp, body := f.browserPost(t, "/"+f.repo+"/issues", cookie, url.Values{
398 398 "csrf": {sess.CSRF}, "title": {"Contributor issue"}, "body": {"from outside"},
399 399 })
400 400 if resp.StatusCode != http.StatusSeeOther {
401 401 t.Fatalf("contributor issue: status=%d body=%s", resp.StatusCode, body)
402 402 }
403 403
404 resp, body = f.browserGet(t, "/r/"+f.repo+"/changes/new", cookie)
404 resp, body = f.browserGet(t, "/"+f.repo+"/changes/new", cookie)
405 405 if resp.StatusCode != http.StatusOK || !strings.Contains(body, headRef) || !strings.Contains(body, "Open change") {
406 406 t.Fatalf("request form: status=%d body=%s", resp.StatusCode, body)
407 407 }
408 408
409 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes", cookie, url.Values{
409 resp, body = f.browserPost(t, "/"+f.repo+"/changes", cookie, url.Values{
410 410 "csrf": {sess.CSRF}, "title": {"Contributor patch"}, "body": {"please review"},
411 411 "base": {"main"}, "head": {headRef},
412 412 })
@@ -427,7 +427,7 @@ func TestContributorNamespaceAndRequestUseTheAccountName(t *testing.T) {
427 427
428 428 // The handler refuses a head outside the contributor namespace even if the
429 429 // form is edited by hand.
430 resp, _ = f.browserPost(t, "/r/"+f.repo+"/changes", cookie, url.Values{
430 resp, _ = f.browserPost(t, "/"+f.repo+"/changes", cookie, url.Values{
431 431 "csrf": {sess.CSRF}, "title": {"Wrong branch"}, "base": {"main"}, "head": {"main"},
432 432 })
433 433 if resp.StatusCode != http.StatusForbidden {
@@ -471,7 +471,7 @@ func TestContributorRepointsRequestTarget(t *testing.T) {
471 471 }
472 472
473 473 sess, cookie := f.browserSession(t, contrib.Name)
474 resp, body := f.browserPost(t, "/r/"+f.repo+"/changes", cookie, url.Values{
474 resp, body := f.browserPost(t, "/"+f.repo+"/changes", cookie, url.Values{
475 475 "csrf": {sess.CSRF}, "title": {"Contributor patch"}, "base": {"main"}, "head": {headRef},
476 476 })
477 477 if resp.StatusCode != http.StatusSeeOther {
@@ -508,11 +508,11 @@ func TestContributorRepointsRequestTarget(t *testing.T) {
508 508 }
509 509
510 510 // The control is offered, with the contributor's own branch as a choice.
511 resp, body = f.browserGet(t, "/r/"+f.repo+"/changes/"+id, cookie)
511 resp, body = f.browserGet(t, "/"+f.repo+"/changes/"+id, cookie)
512 512 if resp.StatusCode != http.StatusOK {
513 513 t.Fatalf("request page: status=%d body=%s", resp.StatusCode, body)
514 514 }
515 if !strings.Contains(body, `action="/r/`+f.repo+`/changes/`+id+`/target"`) || !strings.Contains(body, headRef) {
515 if !strings.Contains(body, `action="/`+f.repo+`/changes/`+id+`/target"`) || !strings.Contains(body, headRef) {
516 516 t.Fatalf("the request page does not offer the target control:\n%s", body)
517 517 }
518 518 // The Review header carries the head commit under review.
@@ -520,7 +520,7 @@ func TestContributorRepointsRequestTarget(t *testing.T) {
520 520 t.Fatalf("the Review header does not show the head commit:\n%s", body)
521 521 }
522 522
523 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+id+"/target", cookie, url.Values{
523 resp, body = f.browserPost(t, "/"+f.repo+"/changes/"+id+"/target", cookie, url.Values{
524 524 "csrf": {sess.CSRF}, "base": {"main"}, "head": {headRef},
525 525 })
526 526 if resp.StatusCode != http.StatusSeeOther {
@@ -536,7 +536,7 @@ func TestContributorRepointsRequestTarget(t *testing.T) {
536 536
537 537 // The namespace rule is the one the create form applies: a contributor cannot
538 538 // point their request at a branch they could not have pushed.
539 resp, _ = f.browserPost(t, "/r/"+f.repo+"/changes/"+id+"/target", cookie, url.Values{
539 resp, _ = f.browserPost(t, "/"+f.repo+"/changes/"+id+"/target", cookie, url.Values{
540 540 "csrf": {sess.CSRF}, "base": {"main"}, "head": {"main"},
541 541 })
542 542 if resp.StatusCode != http.StatusForbidden {
@@ -549,7 +549,7 @@ func TestContributorRepointsRequestTarget(t *testing.T) {
549 549 // A maintainer may move any open request, including a contributor's.
550 550 ownerSess, ownerCookie := f.browserSession(t, f.ownerID)
551 551 mainTip, _ := m.Git().RevParse("refs/heads/main")
552 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+id+"/target", ownerCookie, url.Values{
552 resp, body = f.browserPost(t, "/"+f.repo+"/changes/"+id+"/target", ownerCookie, url.Values{
553 553 "csrf": {ownerSess.CSRF}, "base": {"main"}, "head": {"main"},
554 554 })
555 555 if resp.StatusCode != http.StatusSeeOther {
@@ -565,11 +565,11 @@ func TestContributorRepointsRequestTarget(t *testing.T) {
565 565 if _, errs, err := f.git(base, "push", f.sshURL(f.repo), "main:refs/heads/parent"); err != nil {
566 566 t.Fatalf("parent push: %v\n%s", err, errs)
567 567 }
568 resp, body = f.browserGet(t, "/r/"+f.repo+"/changes/"+id, cookie)
569 if resp.StatusCode != http.StatusOK || !strings.Contains(body, `action="/r/`+f.repo+`/changes/`+id+`/target"`) {
568 resp, body = f.browserGet(t, "/"+f.repo+"/changes/"+id, cookie)
569 if resp.StatusCode != http.StatusOK || !strings.Contains(body, `action="/`+f.repo+`/changes/`+id+`/target"`) {
570 570 t.Fatalf("the request page does not offer the target control:\n%s", body)
571 571 }
572 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+id+"/target", cookie, url.Values{
572 resp, body = f.browserPost(t, "/"+f.repo+"/changes/"+id+"/target", cookie, url.Values{
573 573 "csrf": {sess.CSRF}, "base": {"parent"}, "head": {"main"},
574 574 })
575 575 if resp.StatusCode != http.StatusSeeOther {
@@ -580,13 +580,13 @@ func TestContributorRepointsRequestTarget(t *testing.T) {
580 580 }
581 581
582 582 // Closed requests are settled: the head and base are history, not settings.
583 resp, body = f.browserPost(t, "/r/"+f.repo+"/changes/"+id+"/state", cookie, url.Values{
583 resp, body = f.browserPost(t, "/"+f.repo+"/changes/"+id+"/state", cookie, url.Values{
584 584 "csrf": {sess.CSRF}, "state": {"closed"},
585 585 })
586 586 if resp.StatusCode != http.StatusSeeOther {
587 587 t.Fatalf("close: status=%d body=%s", resp.StatusCode, body)
588 588 }
589 resp, _ = f.browserPost(t, "/r/"+f.repo+"/changes/"+id+"/target", cookie, url.Values{
589 resp, _ = f.browserPost(t, "/"+f.repo+"/changes/"+id+"/target", cookie, url.Values{
590 590 "csrf": {sess.CSRF}, "base": {"main"}, "head": {headRef},
591 591 })
592 592 if resp.StatusCode != http.StatusForbidden {
@@ -652,7 +652,7 @@ jobs:
652 652 }
653 653
654 654 contribSess, contribCookie := f.browserSession(t, contrib.Name)
655 resp, body := f.browserPost(t, "/r/"+f.repo+"/changes", contribCookie, url.Values{
655 resp, body := f.browserPost(t, "/"+f.repo+"/changes", contribCookie, url.Values{
656 656 "csrf": {contribSess.CSRF}, "title": {"CI gate"}, "body": {"please review"},
657 657 "base": {"main"}, "head": {ref},
658 658 })
@@ -664,10 +664,10 @@ jobs:
664 664 t.Fatalf("requests = %v, err = %v", list, err)
665 665 }
666 666 requestID := list[0].ID
667 requestURL := "/r/" + f.repo + "/changes/" + requestID
667 requestURL := "/" + f.repo + "/changes/" + requestID
668 668
669 669 // The Runs tab is history only; the approval belongs with the code under review.
670 resp, body = f.browserGet(t, "/r/"+f.repo+"/runs", contribCookie)
670 resp, body = f.browserGet(t, "/"+f.repo+"/runs", contribCookie)
671 671 if resp.StatusCode != http.StatusOK || strings.Contains(body, "waiting for a maintainer") || strings.Contains(body, "Allow CI</button>") {
672 672 t.Fatalf("runs tab still owns CI approval: status=%d body=%s", resp.StatusCode, body)
673 673 }
@@ -678,7 +678,7 @@ jobs:
678 678 if strings.Contains(body, "Allow CI</button>") {
679 679 t.Fatalf("contributor saw the Allow CI control: body=%s", body)
680 680 }
681 resp, _ = f.browserPost(t, "/r/"+f.repo+"/ci/allow", contribCookie, url.Values{
681 resp, _ = f.browserPost(t, "/"+f.repo+"/ci/allow", contribCookie, url.Values{
682 682 "csrf": {contribSess.CSRF}, "ref": {"refs/heads/" + ref}, "request": {requestID},
683 683 })
684 684 if resp.StatusCode != http.StatusForbidden {
@@ -692,15 +692,15 @@ jobs:
692 692 !strings.Contains(body, `class="success"`) || !strings.Contains(body, "View runs") {
693 693 t.Fatalf("maintainer actions missing from request: status=%d body=%s", resp.StatusCode, body)
694 694 }
695 resp, runsPage := f.browserGet(t, "/r/"+f.repo+"/runs", ownerCookie)
695 resp, runsPage := f.browserGet(t, "/"+f.repo+"/runs", ownerCookie)
696 696 if resp.StatusCode != http.StatusOK || strings.Contains(runsPage, "Allow CI</button>") || strings.Contains(runsPage, "Awaiting CI approval") {
697 697 t.Fatalf("runs tab should not show contributor CI approval: status=%d body=%s", resp.StatusCode, runsPage)
698 698 }
699 resp, codePage := f.browserGet(t, "/r/"+f.repo+"/code", ownerCookie)
699 resp, codePage := f.browserGet(t, "/"+f.repo+"/code", ownerCookie)
700 700 if resp.StatusCode != http.StatusOK || strings.Contains(codePage, "Allow CI</button>") {
701 701 t.Fatalf("contributor CI control should not be in the code browser: status=%d body=%s", resp.StatusCode, codePage)
702 702 }
703 resp, body = f.browserPost(t, "/r/"+f.repo+"/ci/allow", ownerCookie, url.Values{
703 resp, body = f.browserPost(t, "/"+f.repo+"/ci/allow", ownerCookie, url.Values{
704 704 "csrf": {ownerSess.CSRF}, "ref": {"refs/heads/" + ref}, "request": {requestID},
705 705 })
706 706 if resp.StatusCode != http.StatusSeeOther {
@@ -729,23 +729,23 @@ func TestOpenRegistrationPagesAndBegin(t *testing.T) {
729 729 c.OpenRegistration = true
730 730 })
731 731
732 resp, body := f.browserGet(t, "/login", "")
732 resp, body := f.browserGet(t, "/-/login", "")
733 733 if resp.StatusCode != http.StatusOK || !strings.Contains(body, "Create one") {
734 734 t.Fatalf("login page: status=%d body=%s", resp.StatusCode, body)
735 735 }
736 resp, body = f.browserGet(t, "/register", "")
736 resp, body = f.browserGet(t, "/-/register", "")
737 737 if resp.StatusCode != http.StatusOK || !strings.Contains(body, "Create account and passkey") {
738 738 t.Fatalf("register page: status=%d body=%s", resp.StatusCode, body)
739 739 }
740 resp, out := f.browserJSON(t, "/auth/register/begin", "", "", map[string]string{"username": "newbie"})
740 resp, out := f.browserJSON(t, "/-/auth/register/begin", "", "", map[string]string{"username": "newbie"})
741 741 if resp.StatusCode != http.StatusOK || out["publicKey"] == nil {
742 742 t.Fatalf("register begin: status=%d body=%v", resp.StatusCode, out)
743 743 }
744 resp, _ = f.browserJSON(t, "/auth/register/begin", "", "", map[string]string{"username": "keenan"})
744 resp, _ = f.browserJSON(t, "/-/auth/register/begin", "", "", map[string]string{"username": "keenan"})
745 745 if resp.StatusCode != http.StatusConflict {
746 746 t.Fatalf("taken name status = %d, want 409", resp.StatusCode)
747 747 }
748 resp, _ = f.browserJSON(t, "/auth/register/begin", "", "", map[string]string{"username": "Not A Name"})
748 resp, _ = f.browserJSON(t, "/-/auth/register/begin", "", "", map[string]string{"username": "Not A Name"})
749 749 if resp.StatusCode != http.StatusBadRequest {
750 750 t.Fatalf("invalid name status = %d, want 400", resp.StatusCode)
751 751 }
@@ -759,15 +759,15 @@ func TestOpenRegistrationPagesAndBegin(t *testing.T) {
759 759 func TestOpenRegistrationIsOffByDefault(t *testing.T) {
760 760 t.Parallel()
761 761 f := browserFixture(t)
762 resp, body := f.browserGet(t, "/login", "")
762 resp, body := f.browserGet(t, "/-/login", "")
763 763 if resp.StatusCode != http.StatusOK || strings.Contains(body, "Create one") {
764 764 t.Fatalf("login page offered registration while disabled: status=%d body=%s", resp.StatusCode, body)
765 765 }
766 resp, _ = f.browserGet(t, "/register", "")
766 resp, _ = f.browserGet(t, "/-/register", "")
767 767 if resp.StatusCode != http.StatusNotFound {
768 768 t.Fatalf("register page status = %d, want 404", resp.StatusCode)
769 769 }
770 resp, _ = f.browserJSON(t, "/auth/register/begin", "", "", map[string]string{"username": "newbie"})
770 resp, _ = f.browserJSON(t, "/-/auth/register/begin", "", "", map[string]string{"username": "newbie"})
771 771 if resp.StatusCode != http.StatusServiceUnavailable {
772 772 t.Fatalf("register begin status = %d, want 503", resp.StatusCode)
773 773 }
@@ -784,7 +784,7 @@ func TestAccountAddsAndRemovesItsOwnSSHKey(t *testing.T) {
784 784 }
785 785 fp := ssh.FingerprintSHA256(k)
786 786
787 resp, body := f.browserPost(t, "/account/keys", cookie, url.Values{
787 resp, body := f.browserPost(t, "/-/account/keys", cookie, url.Values{
788 788 "csrf": {sess.CSRF}, "key": {pub},
789 789 })
790 790 if resp.StatusCode != http.StatusSeeOther {
@@ -804,7 +804,7 @@ func TestAccountAddsAndRemovesItsOwnSSHKey(t *testing.T) {
804 804 t.Fatalf("added key %s is not on the account: %+v", fp, a.Keys)
805 805 }
806 806
807 resp, body = f.browserPost(t, "/account/keys/remove", cookie, url.Values{
807 resp, body = f.browserPost(t, "/-/account/keys/remove", cookie, url.Values{
808 808 "csrf": {sess.CSRF}, "fingerprint": {fp},
809 809 })
810 810 if resp.StatusCode != http.StatusSeeOther {
@@ -824,7 +824,7 @@ func TestDisplayNameRouteIsGone(t *testing.T) {
824 824 t.Parallel()
825 825 f := browserFixture(t)
826 826 _, cookie := f.browserSession(t, f.ownerID)
827 resp, _ := f.browserPost(t, "/account/display", cookie, url.Values{"display": {"Keenan O'Brien"}})
827 resp, _ := f.browserPost(t, "/-/account/display", cookie, url.Values{"display": {"Keenan O'Brien"}})
828 828 if resp.StatusCode != http.StatusNotFound {
829 829 t.Fatalf("display route status = %d, want 404", resp.StatusCode)
830 830 }
@@ -837,7 +837,7 @@ func TestLogoutRefusesAnOversizedBody(t *testing.T) {
837 837 f := browserFixture(t)
838 838 sess, cookie := f.browserSession(t, f.ownerID)
839 839
840 req, err := http.NewRequest("POST", f.http.URL+"/logout",
840 req, err := http.NewRequest("POST", f.http.URL+"/-/logout",
841 841 strings.NewReader("csrf="+sess.CSRF+"&padding="+strings.Repeat("a", 128<<10)))
842 842 if err != nil {
843 843 t.Fatal(err)
@@ -865,7 +865,7 @@ func TestLoginBeginIsRateLimited(t *testing.T) {
865 865
866 866 var sawOK, sawLimited bool
867 867 for i := 0; i < 100; i++ {
868 resp, _ := f.browserJSON(t, "/auth/login/begin", "", "", map[string]string{"username": "nobody"})
868 resp, _ := f.browserJSON(t, "/-/auth/login/begin", "", "", map[string]string{"username": "nobody"})
869 869 switch resp.StatusCode {
870 870 case http.StatusOK:
871 871 if sawLimited {
@@ -895,7 +895,7 @@ func TestAccountPageIsNotCached(t *testing.T) {
895 895 t.Parallel()
896 896 f := browserFixture(t)
897 897 _, cookie := f.browserSession(t, f.ownerID)
898 resp, _ := f.browserGet(t, "/account", cookie)
898 resp, _ := f.browserGet(t, "/-/account", cookie)
899 899 if got := resp.Header.Get("Cache-Control"); got != "no-store" {
900 900 t.Fatalf("account page Cache-Control = %q", got)
901 901 }
@@ -914,7 +914,7 @@ func TestCrossOriginPostIsRefused(t *testing.T) {
914 914
915 915 post := func(origin string) *http.Response {
916 916 body := strings.NewReader(url.Values{"csrf": {sess.CSRF}}.Encode())
917 req, err := http.NewRequest("POST", f.http.URL+"/logout", body)
917 req, err := http.NewRequest("POST", f.http.URL+"/-/logout", body)
918 918 if err != nil {
919 919 t.Fatal(err)
920 920 }