Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

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.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 54m ago 4b8c9e1d
history blame raw 7.2 KiB
1package config
2
3import (
4 "strings"
5 "testing"
6
7 "rodent.software/vole/forge/identity"
8)
9
10// TestDefaultsAreValid is the promise of a config file that does not exist:
11// `vole serve` works out of the box, so the defaults must pass the same check a
12// hand-written file does.
13func TestDefaultsAreValid(t *testing.T) {
14 cfg, err := Load("")
15 if err != nil {
16 t.Fatal(err)
17 }
18 if err := cfg.Validate(); err != nil {
19 t.Fatalf("built-in defaults do not validate:\n%v", err)
20 }
21}
22
23func TestValidationRefusesUnsafeCombinations(t *testing.T) {
24 t.Parallel()
25 cases := []struct {
26 name string
27 tweak func(*Config)
28 want string
29 }{
30 {
31 name: "dev_login without dev_only",
32 tweak: func(c *Config) { c.DevLogin = "keenan" },
33 want: "dev_login requires dev_only",
34 },
35 {
36 name: "dev_login next to a public url",
37 tweak: func(c *Config) {
38 c.DevOnly, c.DevLogin = true, "keenan"
39 c.PublicURL = "https://forge.example.com"
40 },
41 want: "cannot be combined with a public_url",
42 },
43 {
44 name: "dev_login next to an https listener",
45 tweak: func(c *Config) {
46 c.DevOnly, c.DevLogin = true, "keenan"
47 c.HTTPSAddr = ":443"
48 c.ACMEDomains = []string{"forge.example.com"}
49 },
50 want: "cannot be combined with https_addr",
51 },
52 {
53 name: "dev_login on a public listener",
54 tweak: func(c *Config) {
55 c.DevOnly, c.DevLogin = true, "keenan"
56 c.HTTPAddr = "0.0.0.0:8080"
57 },
58 want: "loopback address",
59 },
60 {
61 name: "dev_login naming nothing",
62 tweak: func(c *Config) {
63 c.DevOnly, c.DevLogin = true, "Keenan!"
64 },
65 want: "must name an account",
66 },
67 {
68 name: "open_registration without a public url",
69 tweak: func(c *Config) { c.OpenRegistration = true },
70 want: "open_registration requires public_url",
71 },
72 {
73 name: "open_registration on plain http",
74 tweak: func(c *Config) {
75 c.OpenRegistration = true
76 c.PublicURL = "http://forge.example.com"
77 },
78 want: "requires an https public_url",
79 },
80 {
81 name: "self-registration granting admin",
82 tweak: func(c *Config) { c.OpenRegistrationRole = "admin" },
83 want: "none, read or contribute",
84 },
85 {
86 name: "an unknown self-registration role",
87 tweak: func(c *Config) { c.OpenRegistrationRole = "root" },
88 want: "unknown role",
89 },
90 {
91 name: "self-registration granting maintain",
92 tweak: func(c *Config) { c.OpenRegistrationRole = "maintain" },
93 want: "none, read or contribute",
94 },
95 {
96 name: "public_url with a path",
97 tweak: func(c *Config) { c.PublicURL = "https://forge.example.com/vole" },
98 want: "no path",
99 },
100 {
101 name: "public_url with credentials",
102 tweak: func(c *Config) { c.PublicURL = "https://user:pw@forge.example.com" },
103 want: "user or password",
104 },
105 {
106 name: "public_url with a query",
107 tweak: func(c *Config) { c.PublicURL = "https://forge.example.com/?x=1" },
108 want: "query or fragment",
109 },
110 {
111 name: "public_url with another scheme",
112 tweak: func(c *Config) { c.PublicURL = "ftp://forge.example.com" },
113 want: "must start with",
114 },
115 {
116 name: "pages_domain as a URL",
117 tweak: func(c *Config) { c.PagesDomain = "https://pages.example.com" },
118 want: "pages_domain",
119 },
120 {
121 name: "pages_domain with a port",
122 tweak: func(c *Config) { c.PagesDomain = "pages.example.com:8080" },
123 want: "pages_domain",
124 },
125 {
126 name: "pages_ref with a space",
127 tweak: func(c *Config) { c.PagesRef = "my pages" },
128 want: "pages_ref",
129 },
130 {
131 name: "a negative size",
132 tweak: func(c *Config) { c.LFSObjectMaxBytes = -1 },
133 want: "must not be negative",
134 },
135 {
136 name: "a push cap smaller than a file cap",
137 tweak: func(c *Config) {
138 c.MaxBlobBytes = 100 << 20
139 c.ReceiveMaxBytes = 1 << 10
140 },
141 want: "smaller than max_blob_bytes",
142 },
143 {
144 name: "a lease longer than an hour",
145 tweak: func(c *Config) { c.LeaseSeconds = 7200 },
146 want: "lease_seconds",
147 },
148 {
149 name: "an http listener",
150 tweak: func(c *Config) { c.HTTPAddr = "8080" },
151 want: "http_addr must be host:port",
152 },
153 {
154 name: "a named port",
155 tweak: func(c *Config) { c.SSHAddr = "127.0.0.1:ssh" },
156 want: "invalid port",
157 },
158 {
159 name: "https with no name to certify",
160 tweak: func(c *Config) { c.HTTPSAddr = ":443" },
161 want: "https_addr needs acme_domains",
162 },
163 {
164 name: "an acme email list",
165 tweak: func(c *Config) { c.ACMEEmail = "a@example.com, b@example.com" },
166 want: "acme_email",
167 },
168 {
169 name: "an acme domain that is a URL",
170 tweak: func(c *Config) {
171 c.HTTPSAddr = ":443"
172 c.ACMEDomains = []string{"https://forge.example.com"}
173 },
174 want: "acme_domains",
175 },
176 {
177 name: "an acme directory that is not https",
178 tweak: func(c *Config) { c.ACMEDirectoryURL = "http://acme.example.com/directory" },
179 want: "acme_directory_url must be https",
180 },
181 }
182 for _, c := range cases {
183 c := c
184 t.Run(c.name, func(t *testing.T) {
185 t.Parallel()
186 cfg, err := Load("")
187 if err != nil {
188 t.Fatal(err)
189 }
190 c.tweak(cfg)
191 err = cfg.Validate()
192 if err == nil {
193 t.Fatalf("%s was accepted", c.name)
194 }
195 if !strings.Contains(err.Error(), c.want) {
196 t.Fatalf("refusal did not say %q:\n%v", c.want, err)
197 }
198 })
199 }
200}
201
202// TestValidationAcceptsDevelopmentConfiguration: a working preview setup has to
203// pass, or the guard becomes something people work around.
204func TestValidationAcceptsDevelopmentConfiguration(t *testing.T) {
205 t.Parallel()
206 cfg, err := Load("")
207 if err != nil {
208 t.Fatal(err)
209 }
210 cfg.DevOnly = true
211 cfg.DevLogin = "keenan"
212 cfg.PublicURL = "http://127.0.0.1:8080"
213 if err := cfg.Validate(); err != nil {
214 t.Fatalf("a development instance was refused:\n%v", err)
215 }
216}
217
218func TestValidationAcceptsProductionConfiguration(t *testing.T) {
219 t.Parallel()
220 cfg, err := Load("")
221 if err != nil {
222 t.Fatal(err)
223 }
224 cfg.PublicURL = "https://forge.example.com/"
225 cfg.OpenRegistration = true
226 cfg.OpenRegistrationRole = "contribute"
227 cfg.HTTPSAddr = ":443"
228 cfg.HTTPAddr = ":80"
229 cfg.ACMEDomains = []string{"forge.example.com", "pages.example.com"}
230 cfg.ACMEEmail = "admin@example.com"
231 cfg.PagesDomain = "pages.example.com"
232 if err := cfg.Validate(); err != nil {
233 t.Fatalf("a production configuration was refused:\n%v", err)
234 }
235}
236
237func TestValidationReportsEveryProblem(t *testing.T) {
238 t.Parallel()
239 cfg, err := Load("")
240 if err != nil {
241 t.Fatal(err)
242 }
243 cfg.PagesDomain = "not a domain"
244 cfg.LeaseSeconds = -5
245 cfg.OpenRegistrationRole = "root"
246 verr := cfg.Validate()
247 if verr == nil {
248 t.Fatal("three bad settings were accepted")
249 }
250 for _, want := range []string{"pages_domain", "lease_seconds", "open_registration_role"} {
251 if !strings.Contains(verr.Error(), want) {
252 t.Errorf("one error did not name %s:\n%v", want, verr)
253 }
254 }
255}
256
257// TestAccountNameRuleMatchesRoster keeps the duplicated rule in step. config is
258// the leaf package, so it does not import the roster just to ask whether a name
259// could be an account.
260func TestAccountNameRuleMatchesRoster(t *testing.T) {
261 t.Parallel()
262 names := []string{
263 "keenan", "ab", "a-b_c9", "", "a", "Keenan", "-keenan", "keenan-", "k" + strings.Repeat("x", 32),
264 "keenan!", "keenan smith", "1keenan",
265 }
266 for _, name := range names {
267 want := identity.ValidateName(name) == nil
268 if got := accountNameRE.MatchString(name); got != want {
269 t.Errorf("accountNameRE(%q) = %v, roster says %v", name, got, want)
270 }
271 }
272}
273