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.
Let instance admins create repositories
455835e7b5d329ca64c96234dfffc7b6702f370e · Keenan Nemetz · 15d ago · parent 395554f4
Let instance admins create repositories The Admin section gains a New repository form: name, owner, and description. Creation uses the same store method as the CLI, seeds the owner as repo admin, updates the index, is audited, and lands on the new repository's access editor. This completes the instance-admin slice in the UI plan.
server/admin_repos_test.go +85
| @@ -0,0 +1,85 @@ | |||
| 1 | package server_test | ||
| 2 | |||
| 3 | import ( | ||
| 4 | "net/url" | ||
| 5 | "os" | ||
| 6 | "strings" | ||
| 7 | "testing" | ||
| 8 | |||
| 9 | "github.com/rodent-software/vole/forge/access" | ||
| 10 | ) | ||
| 11 | |||
| 12 | func TestAdminCreatesRepository(t *testing.T) { | ||
| 13 | t.Parallel() | ||
| 14 | f := browserFixture(t) | ||
| 15 | if _, err := f.srv.Identity.SetAdmin(f.ownerID, true, "test"); err != nil { | ||
| 16 | t.Fatal(err) | ||
| 17 | } | ||
| 18 | f.addWriter(t) | ||
| 19 | sess, cookie := f.browserSession(t, f.ownerID) | ||
| 20 | |||
| 21 | resp, body := f.browserGet(t, "/admin/repos/new", cookie) | ||
| 22 | if resp.StatusCode != 200 || !strings.Contains(body, "New repository") || !strings.Contains(body, "wilbur") { | ||
| 23 | t.Fatalf("new repository page = %d:\n%s", resp.StatusCode, body) | ||
| 24 | } | ||
| 25 | |||
| 26 | form := url.Values{ | ||
| 27 | "csrf": {sess.CSRF}, | ||
| 28 | "name": {"demo-two"}, | ||
| 29 | "owner": {"wilbur"}, | ||
| 30 | "description": {"second repository"}, | ||
| 31 | } | ||
| 32 | resp, _ = f.browserPost(t, "/admin/repos", cookie, form) | ||
| 33 | if resp.StatusCode != 303 || resp.Header.Get("Location") != "/admin/repos/demo-two/access" { | ||
| 34 | t.Fatalf("create = %d %q", resp.StatusCode, resp.Header.Get("Location")) | ||
| 35 | } | ||
| 36 | m, err := f.srv.Repos.Open("demo-two") | ||
| 37 | if err != nil { | ||
| 38 | t.Fatal(err) | ||
| 39 | } | ||
| 40 | if m.Description != "second repository" || m.DefaultBranch != "main" { | ||
| 41 | t.Fatalf("new repository = %+v", m) | ||
| 42 | } | ||
| 43 | acl, err := m.ACL() | ||
| 44 | if err != nil { | ||
| 45 | t.Fatal(err) | ||
| 46 | } | ||
| 47 | if acl.Entries[f.writerID] != "admin" { | ||
| 48 | t.Fatalf("owner role = %v", acl.Entries) | ||
| 49 | } | ||
| 50 | if acl.Can("", access.Read) { | ||
| 51 | t.Fatal("new repository is not private") | ||
| 52 | } | ||
| 53 | entry, err := f.srv.Index.Get("demo-two") | ||
| 54 | if err != nil { | ||
| 55 | t.Fatal(err) | ||
| 56 | } | ||
| 57 | if entry.Description != "second repository" || entry.DefaultBranch != "main" { | ||
| 58 | t.Fatalf("index entry = %+v", entry) | ||
| 59 | } | ||
| 60 | audit, err := os.ReadFile(f.cfg.AuditLog()) | ||
| 61 | if err != nil { | ||
| 62 | t.Fatal(err) | ||
| 63 | } | ||
| 64 | if !strings.Contains(string(audit), "web.admin.repo.create") { | ||
| 65 | t.Fatalf("creation was not audited:\n%s", audit) | ||
| 66 | } | ||
| 67 | |||
| 68 | form.Set("csrf", sess.CSRF) | ||
| 69 | if resp, body := f.browserPost(t, "/admin/repos", cookie, form); resp.StatusCode != 400 || !strings.Contains(body, "already exists") { | ||
| 70 | t.Fatalf("duplicate = %d:\n%s", resp.StatusCode, body) | ||
| 71 | } | ||
| 72 | form.Set("name", "Bad Name") | ||
| 73 | if resp, _ := f.browserPost(t, "/admin/repos", cookie, form); resp.StatusCode != 400 { | ||
| 74 | t.Fatalf("invalid name = %d, want 400", resp.StatusCode) | ||
| 75 | } | ||
| 76 | form.Set("name", "third") | ||
| 77 | form.Set("owner", "nobody") | ||
| 78 | if resp, body := f.browserPost(t, "/admin/repos", cookie, form); resp.StatusCode != 400 || !strings.Contains(body, "active account") { | ||
| 79 | t.Fatalf("unknown owner = %d:\n%s", resp.StatusCode, body) | ||
| 80 | } | ||
| 81 | form.Del("csrf") | ||
| 82 | if resp, _ := f.browserPost(t, "/admin/repos", cookie, form); resp.StatusCode != 403 { | ||
| 83 | t.Fatalf("missing CSRF = %d, want 403", resp.StatusCode) | ||
| 84 | } | ||
| 85 | } | ||