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.
Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys
keenan · 2h ago
4b8c9e1d
| 1 | package config |
| 2 | |
| 3 | import ( |
| 4 | "os" |
| 5 | "path/filepath" |
| 6 | "testing" |
| 7 | ) |
| 8 | |
| 9 | // TestDefaultPathFindsThePerUserInstance is the flow `vole admin init --dir |
| 10 | // ~/.vole` promises: later commands find that config without --config. |
| 11 | func TestDefaultPathFindsThePerUserInstance(t *testing.T) { |
| 12 | home := t.TempDir() |
| 13 | t.Setenv("HOME", home) |
| 14 | t.Setenv("USERPROFILE", home) |
| 15 | t.Setenv("VOLE_CONFIG", "") |
| 16 | t.Chdir(t.TempDir()) |
| 17 | |
| 18 | if got := DefaultPath(); got != "vole.toml" { |
| 19 | t.Fatalf("default path with no instance = %q, want vole.toml", got) |
| 20 | } |
| 21 | |
| 22 | homeConfig := filepath.Join(home, ".vole", "vole.toml") |
| 23 | if err := os.MkdirAll(filepath.Dir(homeConfig), 0o755); err != nil { |
| 24 | t.Fatal(err) |
| 25 | } |
| 26 | if err := os.WriteFile(homeConfig, []byte("data_dir = \"/tmp/vole-data\"\n"), 0o644); err != nil { |
| 27 | t.Fatal(err) |
| 28 | } |
| 29 | if got := DefaultPath(); got != homeConfig { |
| 30 | t.Fatalf("default path = %q, want %q", got, homeConfig) |
| 31 | } |
| 32 | |
| 33 | // A config in the working directory belongs to the checkout or instance the |
| 34 | // operator is standing in, so it wins over the home instance. |
| 35 | if err := os.WriteFile("vole.toml", []byte("data_dir = \"./data\"\n"), 0o644); err != nil { |
| 36 | t.Fatal(err) |
| 37 | } |
| 38 | if got := DefaultPath(); got != "vole.toml" { |
| 39 | t.Fatalf("local config did not win: %q", got) |
| 40 | } |
| 41 | |
| 42 | // An explicit environment choice wins over both. |
| 43 | t.Setenv("VOLE_CONFIG", "/srv/vole/vole.toml") |
| 44 | if got := DefaultPath(); got != "/srv/vole/vole.toml" { |
| 45 | t.Fatalf("VOLE_CONFIG did not win: %q", got) |
| 46 | } |
| 47 | } |
| 48 | |
| 49 | // TestResolvePathKeepsExplicitChoice: an empty path means discover, but a named |
| 50 | // path must never be silently replaced. |
| 51 | func TestResolvePathKeepsExplicitChoice(t *testing.T) { |
| 52 | t.Setenv("VOLE_CONFIG", "/from/env.toml") |
| 53 | if got := ResolvePath("/explicit/vole.toml"); got != "/explicit/vole.toml" { |
| 54 | t.Fatalf("ResolvePath explicit = %q", got) |
| 55 | } |
| 56 | if got := ResolvePath(""); got != "/from/env.toml" { |
| 57 | t.Fatalf("ResolvePath empty = %q", got) |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | // A mistyped TOML tag does not fail anything: the key is simply ignored and the |
| 62 | // default applies, which is invisible until somebody's cap does nothing. So the one |
| 63 | // setting whose absence is a *silent* policy change is pinned here. |
| 64 | func TestMaxJobsPerRepoLoads(t *testing.T) { |
| 65 | t.Parallel() |
| 66 | path := filepath.Join(t.TempDir(), "vole.toml") |
| 67 | if err := os.WriteFile(path, []byte("max_jobs_per_repo = 3\n"), 0o644); err != nil { |
| 68 | t.Fatal(err) |
| 69 | } |
| 70 | c, err := Load(path) |
| 71 | if err != nil { |
| 72 | t.Fatal(err) |
| 73 | } |
| 74 | if c.MaxJobsPerRepo != 3 { |
| 75 | t.Fatalf("MaxJobsPerRepo = %d, want 3", c.MaxJobsPerRepo) |
| 76 | } |
| 77 | |
| 78 | // Absent means no bound, which is the behaviour before the key existed. |
| 79 | unset, err := Load("") |
| 80 | if err != nil { |
| 81 | t.Fatal(err) |
| 82 | } |
| 83 | if unset.MaxJobsPerRepo != 0 { |
| 84 | t.Fatalf("default = %d, want 0 (no cap)", unset.MaxJobsPerRepo) |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | // Automatic HTTPS is opted into by naming an address; the rest of the settings |
| 89 | // only matter once that switch is on, so pin their TOML names as a group. |
| 90 | func TestACMESettingsLoad(t *testing.T) { |
| 91 | t.Parallel() |
| 92 | path := filepath.Join(t.TempDir(), "vole.toml") |
| 93 | body := `https_addr = ":443" |
| 94 | acme_email = "admin@example.com" |
| 95 | acme_domains = ["forge.example.com", "git.example.com"] |
| 96 | acme_cache_dir = "/var/lib/vole/acme" |
| 97 | acme_directory_url = "https://acme-staging-v02.api.letsencrypt.org/directory" |
| 98 | ` |
| 99 | if err := os.WriteFile(path, []byte(body), 0o644); err != nil { |
| 100 | t.Fatal(err) |
| 101 | } |
| 102 | c, err := Load(path) |
| 103 | if err != nil { |
| 104 | t.Fatal(err) |
| 105 | } |
| 106 | if c.HTTPSAddr != ":443" { |
| 107 | t.Fatalf("HTTPSAddr = %q", c.HTTPSAddr) |
| 108 | } |
| 109 | if c.ACMEEmail != "admin@example.com" { |
| 110 | t.Fatalf("ACMEEmail = %q", c.ACMEEmail) |
| 111 | } |
| 112 | if got := len(c.ACMEDomains); got != 2 || c.ACMEDomains[0] != "forge.example.com" { |
| 113 | t.Fatalf("ACMEDomains = %v", c.ACMEDomains) |
| 114 | } |
| 115 | if got := c.ACMECachePath(); got != "/var/lib/vole/acme" { |
| 116 | t.Fatalf("ACMECachePath = %q", got) |
| 117 | } |
| 118 | if c.ACMEDirectoryURL != "https://acme-staging-v02.api.letsencrypt.org/directory" { |
| 119 | t.Fatalf("ACMEDirectoryURL = %q", c.ACMEDirectoryURL) |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | // Load records whether it actually read a file, so the status page can tell an |
| 124 | // operator whether the effective values came from disk or the built-in defaults. |
| 125 | func TestLoadRecordsTheSource(t *testing.T) { |
| 126 | t.Parallel() |
| 127 | path := filepath.Join(t.TempDir(), "vole.toml") |
| 128 | if err := os.WriteFile(path, []byte("data_dir = \"/srv/vole\"\n"), 0o644); err != nil { |
| 129 | t.Fatal(err) |
| 130 | } |
| 131 | c, err := Load(path) |
| 132 | if err != nil { |
| 133 | t.Fatal(err) |
| 134 | } |
| 135 | if !c.Loaded || c.Path != path { |
| 136 | t.Fatalf("loaded config = %+v, want Loaded with path %q", c, path) |
| 137 | } |
| 138 | |
| 139 | missing, err := Load(filepath.Join(t.TempDir(), "missing.toml")) |
| 140 | if err != nil { |
| 141 | t.Fatal(err) |
| 142 | } |
| 143 | if missing.Loaded { |
| 144 | t.Fatal("a missing config file was reported as loaded") |
| 145 | } |
| 146 | } |
| 147 |