vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
| 1 | package server_test |
| 2 | |
| 3 | // robots.txt and the sitemap are the crawl surface: the paths worth fetching, |
| 4 | // and the public repositories and pages behind them. These tests pin the two |
| 5 | // rules that matter: a private repository never appears in either, and a |
| 6 | // repository's own sitemap is the same document for a stranger and for an admin. |
| 7 | |
| 8 | import ( |
| 9 | "encoding/xml" |
| 10 | "net/http" |
| 11 | "strings" |
| 12 | "testing" |
| 13 | ) |
| 14 | |
| 15 | func TestRobotsTxtDescribesThePublicSurface(t *testing.T) { |
| 16 | t.Parallel() |
| 17 | f := newFixture(t) |
| 18 | |
| 19 | resp, body := f.browserGet(t, "/robots.txt", "") |
| 20 | if resp.StatusCode != http.StatusOK { |
| 21 | t.Fatalf("robots.txt = %d\n%s", resp.StatusCode, body) |
| 22 | } |
| 23 | if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") { |
| 24 | t.Fatalf("robots.txt content type = %q", ct) |
| 25 | } |
| 26 | for _, want := range []string{ |
| 27 | "User-agent: *", |
| 28 | "Allow: /", |
| 29 | "Disallow: /-/admin", |
| 30 | "Disallow: /-/account", |
| 31 | "Disallow: /*/settings", |
| 32 | "Disallow: /*/issues/new", |
| 33 | "Disallow: /*.git/", |
| 34 | "Sitemap: " + f.http.URL + "/sitemap.xml", |
| 35 | } { |
| 36 | if !strings.Contains(body, want) { |
| 37 | t.Fatalf("robots.txt is missing %q:\n%s", want, body) |
| 38 | } |
| 39 | } |
| 40 | // Profiles are noindex pages, not disallowed ones: a crawler has to be able |
| 41 | // to fetch them to see the request. |
| 42 | for _, unwanted := range []string{"Disallow: /-/u\n", "Disallow: /-/u/"} { |
| 43 | if strings.Contains(body, unwanted) { |
| 44 | t.Fatalf("robots.txt hides a noindex page with %q:\n%s", unwanted, body) |
| 45 | } |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | func TestSitemapListsPublicRepositoriesAndPages(t *testing.T) { |
| 50 | t.Parallel() |
| 51 | f := newFixture(t) |
| 52 | f.publish(f.repo) |
| 53 | |
| 54 | private, err := f.srv.Repos.Create("secret", f.ownerID, "private repo") |
| 55 | if err != nil { |
| 56 | t.Fatal(err) |
| 57 | } |
| 58 | if err := f.srv.Index.Upsert(private); err != nil { |
| 59 | t.Fatal(err) |
| 60 | } |
| 61 | |
| 62 | resp, body := f.browserGet(t, "/sitemap.xml", "") |
| 63 | if resp.StatusCode != http.StatusOK { |
| 64 | t.Fatalf("sitemap.xml = %d\n%s", resp.StatusCode, body) |
| 65 | } |
| 66 | if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/xml") { |
| 67 | t.Fatalf("sitemap.xml content type = %q", ct) |
| 68 | } |
| 69 | for _, want := range []string{ |
| 70 | `<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`, |
| 71 | "<loc>" + f.http.URL + "/sitemap-pages.xml</loc>", |
| 72 | "<loc>" + f.http.URL + "/demo/sitemap.xml</loc>", |
| 73 | } { |
| 74 | if !strings.Contains(body, want) { |
| 75 | t.Fatalf("sitemap index is missing %q:\n%s", want, body) |
| 76 | } |
| 77 | } |
| 78 | if strings.Contains(body, "secret") { |
| 79 | t.Fatalf("sitemap index names a private repository:\n%s", body) |
| 80 | } |
| 81 | |
| 82 | // The answer belongs to the instance, not the session: an admin asking for |
| 83 | // it must not add a private repository to a cacheable document. |
| 84 | _, cookie := f.browserSession(t, f.ownerID) |
| 85 | if _, signedIn := f.browserGet(t, "/sitemap.xml", cookie); strings.Contains(signedIn, "secret") { |
| 86 | t.Fatalf("a signed-in sitemap names a private repository:\n%s", signedIn) |
| 87 | } |
| 88 | if code, _ := f.get("/secret/sitemap.xml"); code != http.StatusNotFound { |
| 89 | t.Fatalf("private repository sitemap = %d, want 404", code) |
| 90 | } |
| 91 | resp, _ = f.browserGet(t, "/secret/sitemap.xml", cookie) |
| 92 | if resp.StatusCode != http.StatusNotFound { |
| 93 | t.Fatalf("signed-in private repository sitemap = %d, want 404", resp.StatusCode) |
| 94 | } |
| 95 | |
| 96 | _, body = f.browserGet(t, "/sitemap-pages.xml", "") |
| 97 | for _, want := range []string{ |
| 98 | "<loc>" + f.http.URL + "/</loc>", |
| 99 | "<loc>" + f.http.URL + "/-/activity</loc>", |
| 100 | } { |
| 101 | if !strings.Contains(body, want) { |
| 102 | t.Fatalf("page sitemap is missing %q:\n%s", want, body) |
| 103 | } |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | func TestRepoSitemapListsIssuesChangesAndReleases(t *testing.T) { |
| 108 | t.Parallel() |
| 109 | f := newFixture(t) |
| 110 | f.publish(f.repo) |
| 111 | |
| 112 | work := workRepo(t, map[string]string{"readme.md": "sitemap\n"}) |
| 113 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil { |
| 114 | t.Fatalf("main push: %v\n%s", err, errs) |
| 115 | } |
| 116 | out, _, _ := runGit(t, work, "rev-parse", "HEAD") |
| 117 | head := strings.TrimSpace(out) |
| 118 | if _, errs, err := runGit(t, work, "tag", "-a", "v1.0.0", "-m", "first release\n"); err != nil { |
| 119 | t.Fatalf("tag: %v\n%s", err, errs) |
| 120 | } |
| 121 | if _, errs, err := f.git(work, "push", f.sshURL(f.repo), "refs/tags/v1.0.0:refs/tags/v1.0.0"); err != nil { |
| 122 | t.Fatalf("tag push: %v\n%s", err, errs) |
| 123 | } |
| 124 | |
| 125 | const issueID = "01M5JR2JK29KKMAA1T9MDNTS01" |
| 126 | const changeID = "01M5JR2JK29KKMAA1T9MDNTS02" |
| 127 | meta := f.newMetaRepo(t) |
| 128 | if err := meta.apply(map[string]string{ |
| 129 | "issues/" + issueID + "/issue.md": issueFile("Sitemap issue", "keenan", "open", "2026-10-22T21:00:00Z", []string{"web"}, "body\n"), |
| 130 | "changes/" + changeID + "/change.md": requestFileTitled("Sitemap change", "main", head, "keenan", "body\n"), |
| 131 | }, "sitemap fixtures"); err != nil { |
| 132 | t.Fatalf("metadata push: %v", err) |
| 133 | } |
| 134 | |
| 135 | resp, body := f.browserGet(t, "/"+f.repo+"/sitemap.xml", "") |
| 136 | if resp.StatusCode != http.StatusOK { |
| 137 | t.Fatalf("repository sitemap = %d\n%s", resp.StatusCode, body) |
| 138 | } |
| 139 | base := f.http.URL |
| 140 | for _, want := range []string{ |
| 141 | `<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">`, |
| 142 | "<loc>" + base + "/demo</loc>", |
| 143 | "<loc>" + base + "/demo/activity</loc>", |
| 144 | "<loc>" + base + "/demo/code</loc>", |
| 145 | "<loc>" + base + "/demo/issues</loc>", |
| 146 | "<loc>" + base + "/demo/issues/" + issueID + "</loc>", |
| 147 | "<loc>" + base + "/demo/changes</loc>", |
| 148 | "<loc>" + base + "/demo/changes/" + changeID + "</loc>", |
| 149 | "<loc>" + base + "/demo/releases</loc>", |
| 150 | "<loc>" + base + "/demo/releases/v1.0.0</loc>", |
| 151 | "<loc>" + base + "/demo/commits/main</loc>", |
| 152 | "<lastmod>", |
| 153 | } { |
| 154 | if !strings.Contains(body, want) { |
| 155 | t.Fatalf("repository sitemap is missing %q:\n%s", want, body) |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | var set struct { |
| 160 | URLs []struct { |
| 161 | Loc string `xml:"loc"` |
| 162 | } `xml:"url"` |
| 163 | } |
| 164 | if err := xml.Unmarshal([]byte(body), &set); err != nil { |
| 165 | t.Fatalf("repository sitemap is not XML: %v\n%s", err, body) |
| 166 | } |
| 167 | if len(set.URLs) < 9 { |
| 168 | t.Fatalf("repository sitemap has %d URLs, want the repository, list, issue, change and release pages:\n%s", len(set.URLs), body) |
| 169 | } |
| 170 | } |
| 171 |