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.
Summarize accounts, activity, and configuration on the Admin overview
0db980ee6b820127a0310be661b5d2c98fcdde92 · Keenan Nemetz · 4d ago · parent 43e681d8
Summarize accounts, activity, and configuration on the Admin overview The overview is the whole admin surface at a glance now, so it repeats the head of the other tabs: active people, bots, instance admins, and retired accounts from the roster; the newest audit records with their detail and a link into the log; and the configuration warnings, including a browser that cannot sign anyone in. Refs #198 (01M6MK0NNZ2XR1D5TQKNWYT4HN).
server/admin_page_test.go +112
| @@ -4,10 +4,13 @@ import ( | |||
| 4 | 4 | "fmt" | |
| 5 | 5 | "net/http" | |
| 6 | 6 | "os" | |
| 7 | "path/filepath" | ||
| 7 | 8 | "strings" | |
| 8 | 9 | "testing" | |
| 9 | 10 | "time" | |
| 10 | 11 | ||
| 12 | "github.com/rodent-software/vole/forge/access" | ||
| 13 | "github.com/rodent-software/vole/forge/config" | ||
| 11 | 14 | "github.com/rodent-software/vole/internal/audit" | |
| 12 | 15 | ) | |
| 13 | 16 | ||
| @@ -203,3 +206,112 @@ func TestAdminAuditLogEscapesDetailAndIsNotCached(t *testing.T) { | |||
| 203 | 206 | t.Fatalf("audit log Cache-Control = %q, want no-store", got) | |
| 204 | 207 | } | |
| 205 | 208 | } | |
| 209 | |||
| 210 | // cardValue reads the <strong> value that follows a card's muted label. The | ||
| 211 | // overview repeats counts as cards, so a test has to look past the label to say | ||
| 212 | // which number belongs to it. | ||
| 213 | func cardValue(body, label string) string { | ||
| 214 | idx := strings.Index(body, label) | ||
| 215 | if idx < 0 { | ||
| 216 | return "" | ||
| 217 | } | ||
| 218 | rest := body[idx:] | ||
| 219 | start := strings.Index(rest, "<strong>") | ||
| 220 | if start < 0 { | ||
| 221 | return "" | ||
| 222 | } | ||
| 223 | rest = rest[start+len("<strong>"):] | ||
| 224 | end := strings.Index(rest, "</strong>") | ||
| 225 | if end < 0 { | ||
| 226 | return "" | ||
| 227 | } | ||
| 228 | return strings.TrimSpace(rest[:end]) | ||
| 229 | } | ||
| 230 | |||
| 231 | func TestAdminOverviewSummarizesAccountsAndRecentActivity(t *testing.T) { | ||
| 232 | t.Parallel() | ||
| 233 | f := newFixture(t) | ||
| 234 | if _, err := f.srv.Identity.SetAdmin(f.ownerID, true, "test"); err != nil { | ||
| 235 | t.Fatal(err) | ||
| 236 | } | ||
| 237 | f.addWriter(t) | ||
| 238 | pub, _ := keypair(t, filepath.Join(f.cfg.DataDir, "id_bot")) | ||
| 239 | if _, err := f.srv.Identity.CreateBot("ci", access.Contribute, pub, "test"); err != nil { | ||
| 240 | t.Fatal(err) | ||
| 241 | } | ||
| 242 | if _, err := f.srv.Identity.Retire(f.writerID, "test"); err != nil { | ||
| 243 | t.Fatal(err) | ||
| 244 | } | ||
| 245 | _, cookie := f.browserSession(t, f.ownerID) | ||
| 246 | seedAudit(t, f.cfg.AuditLog(), 8, "overview") | ||
| 247 | |||
| 248 | resp, body := f.browserGet(t, "/admin", cookie) | ||
| 249 | if resp.StatusCode != http.StatusOK { | ||
| 250 | t.Fatalf("GET /admin = %d:\n%s", resp.StatusCode, body) | ||
| 251 | } | ||
| 252 | for label, want := range map[string]string{ | ||
| 253 | "Active people": "1", | ||
| 254 | "Bots": "1", | ||
| 255 | "Instance admins": "1", | ||
| 256 | "Retired": "1", | ||
| 257 | } { | ||
| 258 | if got := cardValue(body, label); got != want { | ||
| 259 | t.Errorf("%s = %q, want %q:\n%s", label, got, want, body) | ||
| 260 | } | ||
| 261 | } | ||
| 262 | // The overview shows the newest few records, not the whole log. | ||
| 263 | for _, want := range []string{"overview-7", "overview-2", "Read the full audit log"} { | ||
| 264 | if !strings.Contains(body, want) { | ||
| 265 | t.Fatalf("the overview does not show %q:\n%s", want, body) | ||
| 266 | } | ||
| 267 | } | ||
| 268 | for _, gone := range []string{"overview-1", "overview-0"} { | ||
| 269 | if strings.Contains(body, gone) { | ||
| 270 | t.Fatalf("the overview reached past its recent-activity limit: %q:\n%s", gone, body) | ||
| 271 | } | ||
| 272 | } | ||
| 273 | } | ||
| 274 | |||
| 275 | func TestAdminOverviewShowsConfigurationWarnings(t *testing.T) { | ||
| 276 | t.Parallel() | ||
| 277 | f := newFixtureWith(t, func(c *config.Config) { | ||
| 278 | c.DevOnly = true | ||
| 279 | c.DevLogin = "keenan" | ||
| 280 | }) | ||
| 281 | if _, err := f.srv.Identity.SetAdmin(f.ownerID, true, "test"); err != nil { | ||
| 282 | t.Fatal(err) | ||
| 283 | } | ||
| 284 | _, cookie := f.browserSession(t, f.ownerID) | ||
| 285 | |||
| 286 | resp, body := f.browserGet(t, "/admin", cookie) | ||
| 287 | if resp.StatusCode != http.StatusOK { | ||
| 288 | t.Fatalf("GET /admin = %d:\n%s", resp.StatusCode, body) | ||
| 289 | } | ||
| 290 | for _, want := range []string{ | ||
| 291 | "Configuration attention", "dev_only", "dev_login", "browser sign-in", | ||
| 292 | "See every effective setting", | ||
| 293 | } { | ||
| 294 | if !strings.Contains(body, want) { | ||
| 295 | t.Fatalf("the overview does not show the configuration warning %q:\n%s", want, body) | ||
| 296 | } | ||
| 297 | } | ||
| 298 | } | ||
| 299 | |||
| 300 | func TestAdminOverviewOmitsConfigurationAttentionWhenClean(t *testing.T) { | ||
| 301 | t.Parallel() | ||
| 302 | f := newFixtureWith(t, func(c *config.Config) { | ||
| 303 | c.PublicURL = "https://forge.example.com" | ||
| 304 | }) | ||
| 305 | if _, err := f.srv.Identity.SetAdmin(f.ownerID, true, "test"); err != nil { | ||
| 306 | t.Fatal(err) | ||
| 307 | } | ||
| 308 | _, cookie := f.browserSession(t, f.ownerID) | ||
| 309 | |||
| 310 | resp, body := f.browserGet(t, "/admin", cookie) | ||
| 311 | if resp.StatusCode != http.StatusOK { | ||
| 312 | t.Fatalf("GET /admin = %d:\n%s", resp.StatusCode, body) | ||
| 313 | } | ||
| 314 | if strings.Contains(body, "Configuration attention") { | ||
| 315 | t.Fatalf("a clean configuration still shows a warnings section:\n%s", body) | ||
| 316 | } | ||
| 317 | } | ||