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.
Expand the admin status page into an operational dashboard
4506a4d3d279556435fdd414265e7499c3dada16 · Keenan Nemetz · 14d ago · parent 2e024cc9
server/status_test.go +143−4
| @@ -1,23 +1,162 @@ | |||
| 1 | 1 | package server_test | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | "net/http" | ||
| 4 | 5 | "strings" | |
| 5 | 6 | "testing" | |
| 7 | "time" | ||
| 8 | |||
| 9 | "github.com/rodent-software/vole/forge/config" | ||
| 10 | "github.com/rodent-software/vole/forge/repo" | ||
| 6 | 11 | ) | |
| 7 | 12 | ||
| 13 | func (f *fixture) adminGet(t *testing.T, path string) (int, string) { | ||
| 14 | t.Helper() | ||
| 15 | if _, err := f.srv.Identity.SetAdmin(f.ownerID, true, "test"); err != nil { | ||
| 16 | t.Fatal(err) | ||
| 17 | } | ||
| 18 | _, cookie := f.browserSession(t, f.ownerID) | ||
| 19 | resp, body := f.browserGet(t, path, cookie) | ||
| 20 | return resp.StatusCode, body | ||
| 21 | } | ||
| 22 | |||
| 8 | 23 | func TestStatusPageSummarizesTheInstance(t *testing.T) { | |
| 9 | 24 | t.Parallel() | |
| 10 | 25 | f := newFixture(t) | |
| 11 | if code, body := f.get("/status"); code != 200 || !strings.Contains(body, "All core services are responding") { | ||
| 26 | code, body := f.adminGet(t, "/status") | ||
| 27 | if code != 200 { | ||
| 12 | 28 | t.Fatalf("status page = %d:\n%s", code, body) | |
| 13 | 29 | } | |
| 14 | _, body := f.get("/status") | ||
| 15 | for _, want := range []string{"Accounts", "Repositories", "Queued runs", "Runners", "identity", "queue"} { | ||
| 30 | for _, want := range []string{"Overview", "Storage", "Runners", "Configuration", "Storage used", "Free space", "LFS objects", "Page views", "Unique users", "Today (UTC)"} { | ||
| 16 | 31 | if !strings.Contains(body, want) { | |
| 17 | t.Fatalf("status page does not show %q:\n%s", want, body) | ||
| 32 | t.Fatalf("status overview does not show %q:\n%s", want, body) | ||
| 33 | } | ||
| 34 | } | ||
| 35 | for _, gone := range []string{"Service health", "All core services are responding", "identity", "Runners and queue", "Instance and access"} { | ||
| 36 | if strings.Contains(body, gone) { | ||
| 37 | t.Fatalf("status overview shows content from another tab or removed health text %q:\n%s", gone, body) | ||
| 18 | 38 | } | |
| 19 | 39 | } | |
| 20 | 40 | if strings.Contains(body, f.repo) { | |
| 21 | 41 | t.Fatalf("status page named a repository:\n%s", body) | |
| 22 | 42 | } | |
| 23 | 43 | } | |
| 44 | |||
| 45 | func TestStatusPageShowsConfigurationToAdmins(t *testing.T) { | ||
| 46 | t.Parallel() | ||
| 47 | f := newFixtureWith(t, func(c *config.Config) { | ||
| 48 | c.InstanceName = "Acme Forge" | ||
| 49 | c.PagesDomain = "pages.example.com" | ||
| 50 | c.MaxBlobBytes = 1234 | ||
| 51 | }) | ||
| 52 | |||
| 53 | // The page is instance-admin only: anonymous goes to login, and a signed-in | ||
| 54 | // non-admin gets the same 404 as a missing page. | ||
| 55 | if resp, _ := f.browserGet(t, "/status", ""); resp.StatusCode != http.StatusSeeOther || !strings.Contains(resp.Header.Get("Location"), "/login") { | ||
| 56 | t.Fatalf("anonymous status page = %d %q", resp.StatusCode, resp.Header.Get("Location")) | ||
| 57 | } | ||
| 58 | f.addWriter(t) | ||
| 59 | _, writerCookie := f.browserSession(t, f.writerID) | ||
| 60 | if resp, _ := f.browserGet(t, "/status?tab=configuration", writerCookie); resp.StatusCode != http.StatusNotFound { | ||
| 61 | t.Fatalf("non-admin status page = %d, want 404", resp.StatusCode) | ||
| 62 | } | ||
| 63 | |||
| 64 | if _, err := f.srv.Identity.SetAdmin(f.ownerID, true, "test"); err != nil { | ||
| 65 | t.Fatal(err) | ||
| 66 | } | ||
| 67 | _, cookie := f.browserSession(t, f.ownerID) | ||
| 68 | resp, body := f.browserGet(t, "/status?tab=configuration", cookie) | ||
| 69 | if resp.StatusCode != 200 { | ||
| 70 | t.Fatalf("admin status page = %d:\n%s", resp.StatusCode, body) | ||
| 71 | } | ||
| 72 | for _, want := range []string{ | ||
| 73 | "Configuration", "Instance and access", "Acme Forge", "pages.example.com", | ||
| 74 | "max_blob_bytes", "1.2 KiB (1234 bytes)", "built-in defaults", | ||
| 75 | } { | ||
| 76 | if !strings.Contains(body, want) { | ||
| 77 | t.Fatalf("admin status page does not show %q:\n%s", want, body) | ||
| 78 | } | ||
| 79 | } | ||
| 80 | } | ||
| 81 | |||
| 82 | func TestStatusPageCountsPageViews(t *testing.T) { | ||
| 83 | t.Parallel() | ||
| 84 | f := newFixture(t) | ||
| 85 | |||
| 86 | for i := 0; i < 3; i++ { | ||
| 87 | if code, _ := f.get("/"); code != 200 { | ||
| 88 | t.Fatalf("anonymous page %d = %d", i, code) | ||
| 89 | } | ||
| 90 | } | ||
| 91 | _, cookie := f.browserSession(t, f.ownerID) | ||
| 92 | if resp, _ := f.browserGet(t, "/", cookie); resp.StatusCode != 200 { | ||
| 93 | t.Fatalf("signed-in page = %d", resp.StatusCode) | ||
| 94 | } | ||
| 95 | |||
| 96 | stats, err := f.srv.Web.PageViews.Stats(time.Now().UTC()) | ||
| 97 | if err != nil { | ||
| 98 | t.Fatal(err) | ||
| 99 | } | ||
| 100 | if stats.Today.Views != 4 || stats.Today.Users != 1 { | ||
| 101 | t.Fatalf("today = %+v, want 4 views and 1 signed-in user", stats.Today) | ||
| 102 | } | ||
| 103 | |||
| 104 | // The status page must not count itself, or a monitor turns the metric into | ||
| 105 | // a measure of polling. | ||
| 106 | if code, _ := f.adminGet(t, "/status"); code != 200 { | ||
| 107 | t.Fatalf("status = %d", code) | ||
| 108 | } | ||
| 109 | stats, err = f.srv.Web.PageViews.Stats(time.Now().UTC()) | ||
| 110 | if err != nil { | ||
| 111 | t.Fatal(err) | ||
| 112 | } | ||
| 113 | if stats.Today.Views != 4 { | ||
| 114 | t.Fatalf("status page changed the view count to %d", stats.Today.Views) | ||
| 115 | } | ||
| 116 | } | ||
| 117 | |||
| 118 | func TestStatusPageTabsSplitTheSections(t *testing.T) { | ||
| 119 | t.Parallel() | ||
| 120 | f := newFixture(t) | ||
| 121 | if _, err := f.srv.Repos.Create("private", f.ownerID, "private example"); err != nil { | ||
| 122 | t.Fatal(err) | ||
| 123 | } | ||
| 124 | if err := f.srv.Index.Upsert(mustRepoNamed(t, f, "private")); err != nil { | ||
| 125 | t.Fatal(err) | ||
| 126 | } | ||
| 127 | |||
| 128 | code, storage := f.adminGet(t, "/status?tab=storage") | ||
| 129 | if code != 200 || !strings.Contains(storage, "Repositories") || !strings.Contains(storage, "LFS and build objects") { | ||
| 130 | t.Fatalf("storage tab = %d:\n%s", code, storage) | ||
| 131 | } | ||
| 132 | for _, gone := range []string{"Page views", "Runners and queue", "Instance and access"} { | ||
| 133 | if strings.Contains(storage, gone) { | ||
| 134 | t.Fatalf("storage tab shows %q:", gone) | ||
| 135 | } | ||
| 136 | } | ||
| 137 | |||
| 138 | code, runners := f.adminGet(t, "/status?tab=runners") | ||
| 139 | if code != 200 || !strings.Contains(runners, "Runners and queue") || !strings.Contains(runners, "Queue") { | ||
| 140 | t.Fatalf("runners tab = %d:\n%s", code, runners) | ||
| 141 | } | ||
| 142 | if strings.Contains(runners, "Storage used") || strings.Contains(runners, "Page views") { | ||
| 143 | t.Fatalf("runners tab shows overview content:\n%s", runners) | ||
| 144 | } | ||
| 145 | |||
| 146 | code, config := f.adminGet(t, "/status?tab=configuration") | ||
| 147 | if code != 200 || !strings.Contains(config, "Configuration") || !strings.Contains(config, "Instance and access") { | ||
| 148 | t.Fatalf("configuration tab = %d:\n%s", code, config) | ||
| 149 | } | ||
| 150 | if strings.Contains(config, "Storage used") || strings.Contains(config, "Runners and queue") { | ||
| 151 | t.Fatalf("configuration tab shows another tab's content:\n%s", config) | ||
| 152 | } | ||
| 153 | } | ||
| 154 | |||
| 155 | func mustRepoNamed(t *testing.T, f *fixture, name string) *repo.Meta { | ||
| 156 | t.Helper() | ||
| 157 | m, err := f.srv.Repos.Open(name) | ||
| 158 | if err != nil { | ||
| 159 | t.Fatal(err) | ||
| 160 | } | ||
| 161 | return m | ||
| 162 | } | ||