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.
|
c5902451
Keenan Nemetz
16d ago
|
1 | package forge |
| 2 | ||
| 3 | import ( | |
|
c36d5073
Keenan Nemetz
16d ago
|
4 | "crypto/ed25519" |
| 5 | "crypto/rand" | |
|
c5902451
Keenan Nemetz
16d ago
|
6 | "os" |
| 7 | "path/filepath" | |
|
c36d5073
Keenan Nemetz
16d ago
|
8 | "strings" |
|
c5902451
Keenan Nemetz
16d ago
|
9 | "testing" |
| 10 | ||
|
c36d5073
Keenan Nemetz
16d ago
|
11 | "golang.org/x/crypto/ssh" |
|
d37d301f
Keenan Nemetz
18h ago
|
12 | "rodent.software/vole/forge/config" |
|
c5902451
Keenan Nemetz
16d ago
|
13 | ) |
| 14 | ||
| 15 | func TestInitOpenAndRepoLifecycle(t *testing.T) { | |
| 16 | t.Parallel() | |
| 17 | dir := t.TempDir() | |
| 18 | root, err := Init(dir) | |
| 19 | if err != nil { | |
| 20 | t.Fatalf("init: %v", err) | |
| 21 | } | |
| 22 | cfg, err := config.Load(filepath.Join(root, "vole.toml")) | |
| 23 | if err != nil { | |
| 24 | t.Fatal(err) | |
| 25 | } | |
| 26 | inst, err := Open(cfg, "vole") | |
| 27 | if err != nil { | |
| 28 | t.Fatalf("open: %v", err) | |
| 29 | } | |
| 30 | defer inst.Close() | |
| 31 | ||
|
3e7c5e67
Keenan Nemetz
1d ago
|
32 | // init creates the instance repository with the starter files, and indexes it |
| 33 | // so the home page lists it like any other repository. | |
| 34 | theme, err := inst.Repos.Open(cfg.InstanceRepo) | |
| 35 | if err != nil { | |
| 36 | t.Fatalf("init did not create the instance repository: %v", err) | |
| 37 | } | |
| 38 | if _, err := inst.Index.Get(cfg.InstanceRepo); err != nil { | |
| 39 | t.Fatalf("the instance repository is not indexed: %v", err) | |
| 40 | } | |
| 41 | readme, err := theme.Git().BlobAt("refs/heads/"+theme.DefaultBranch, "README.md") | |
| 42 | if err != nil || !strings.Contains(string(readme), "Instance customization") { | |
| 43 | t.Fatalf("instance README = %q, %v", readme, err) | |
| 44 | } | |
|
bc95767f
Keenan Nemetz
1d ago
|
45 | custom, err := theme.Git().BlobAt("refs/heads/"+theme.DefaultBranch, "profile/custom.css") |
| 46 | if err != nil || !strings.Contains(string(custom), ":root[data-theme]") { | |
| 47 | t.Fatalf("starter custom.css does not show the override selector: %q, %v", custom, err) | |
| 48 | } | |
|
d222b0cf
Keenan Nemetz
1d ago
|
49 | if !strings.Contains(string(readme), "profile/themes/<id>.css") { |
| 50 | t.Fatal("the starter README does not document the theme file convention") | |
|
f7a34d87
Keenan Nemetz
1d ago
|
51 | } |
|
3e7c5e67
Keenan Nemetz
1d ago
|
52 | |
|
c36d5073
Keenan Nemetz
16d ago
|
53 | pub, _, err := ed25519.GenerateKey(rand.Reader) |
| 54 | if err != nil { | |
| 55 | t.Fatal(err) | |
| 56 | } | |
| 57 | key, err := ssh.NewPublicKey(pub) | |
| 58 | if err != nil { | |
| 59 | t.Fatal(err) | |
| 60 | } | |
|
62576fa8
Keenan Nemetz
14d ago
|
61 | alice, err := inst.Identity.Create("alice", strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key))), "test") |
|
c5902451
Keenan Nemetz
16d ago
|
62 | if err != nil { |
| 63 | t.Fatal(err) | |
| 64 | } | |
|
62576fa8
Keenan Nemetz
14d ago
|
65 | m, err := inst.Repos.Create("demo", alice.Name, "a demo") |
|
c5902451
Keenan Nemetz
16d ago
|
66 | if err != nil { |
| 67 | t.Fatal(err) | |
| 68 | } | |
| 69 | if err := inst.Index.Upsert(m); err != nil { | |
| 70 | t.Fatal(err) | |
| 71 | } | |
| 72 | if _, err := inst.Index.Get("demo"); err != nil { | |
| 73 | t.Fatalf("index does not know the repo: %v", err) | |
| 74 | } | |
| 75 | ||
| 76 | if err := inst.RenameRepo("demo", "renamed"); err != nil { | |
| 77 | t.Fatalf("rename: %v", err) | |
| 78 | } | |
| 79 | if _, err := inst.Repos.Open("renamed"); err != nil { | |
| 80 | t.Fatalf("renamed repo is missing: %v", err) | |
| 81 | } | |
| 82 | if _, err := inst.Index.Get("renamed"); err != nil { | |
| 83 | t.Fatalf("index did not follow the rename: %v", err) | |
| 84 | } | |
| 85 | ||
|
b1499c72
Keenan Nemetz
13d ago
|
86 | if err := inst.DeleteRepo("renamed", "keenan"); err != nil { |
|
c5902451
Keenan Nemetz
16d ago
|
87 | t.Fatalf("delete: %v", err) |
| 88 | } | |
| 89 | if _, err := inst.Repos.Open("renamed"); !os.IsNotExist(err) { | |
| 90 | t.Fatalf("repo survived delete: %v", err) | |
| 91 | } | |
| 92 | if _, err := inst.Index.Get("renamed"); err == nil { | |
| 93 | t.Fatal("index row survived delete") | |
| 94 | } | |
| 95 | } | |
|
b1499c72
Keenan Nemetz
13d ago
|
96 | |
| 97 | // An instance that cannot write its audit log refuses to start. Discovering that | |
| 98 | // during an incident is too late: every privileged change it served would have | |
| 99 | // been unrecorded. | |
| 100 | func TestOpenRefusesAnUnwritableAuditLog(t *testing.T) { | |
| 101 | t.Parallel() | |
| 102 | dir := t.TempDir() | |
| 103 | root, err := Init(dir) | |
| 104 | if err != nil { | |
| 105 | t.Fatalf("init: %v", err) | |
| 106 | } | |
| 107 | cfg, err := config.Load(filepath.Join(root, "vole.toml")) | |
| 108 | if err != nil { | |
| 109 | t.Fatal(err) | |
| 110 | } | |
| 111 | if err := os.RemoveAll(cfg.AuditLog()); err != nil { | |
| 112 | t.Fatal(err) | |
| 113 | } | |
| 114 | if err := os.Mkdir(cfg.AuditLog(), 0o700); err != nil { | |
| 115 | t.Fatal(err) | |
| 116 | } | |
| 117 | if inst, err := Open(cfg, "vole"); err == nil { | |
| 118 | inst.Close() | |
| 119 | t.Fatal("an instance opened with a directory in place of its audit log") | |
| 120 | } | |
| 121 | } | |
| 122 | ||
| 123 | // A symlink lets somebody outside the service point the log at /dev/null, which | |
| 124 | // is the same as not having one. | |
| 125 | func TestOpenRefusesASymlinkedAuditLog(t *testing.T) { | |
| 126 | t.Parallel() | |
| 127 | dir := t.TempDir() | |
| 128 | root, err := Init(dir) | |
| 129 | if err != nil { | |
| 130 | t.Fatalf("init: %v", err) | |
| 131 | } | |
| 132 | cfg, err := config.Load(filepath.Join(root, "vole.toml")) | |
| 133 | if err != nil { | |
| 134 | t.Fatal(err) | |
| 135 | } | |
| 136 | if err := os.RemoveAll(cfg.AuditLog()); err != nil { | |
| 137 | t.Fatal(err) | |
| 138 | } | |
| 139 | target := filepath.Join(dir, "elsewhere.log") | |
| 140 | if err := os.WriteFile(target, nil, 0o600); err != nil { | |
| 141 | t.Fatal(err) | |
| 142 | } | |
| 143 | if err := os.Symlink(target, cfg.AuditLog()); err != nil { | |
| 144 | t.Fatal(err) | |
| 145 | } | |
| 146 | if inst, err := Open(cfg, "vole"); err == nil { | |
| 147 | inst.Close() | |
| 148 | t.Fatal("an instance opened with a symlinked audit log") | |
| 149 | } | |
| 150 | } |