1package brokerconfig
2
3import (
4 "context"
5 "encoding/json"
6 "os"
7 "path/filepath"
8 "strings"
9 "testing"
10 "time"
11
12 "github.com/stretchr/testify/require"
13
14 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/ports"
16 "github.com/bnema/vev/internal/protocol"
17)
18
19const (
20 testIncarnation = "0102030405060708090a0b0c0d0e0f10"
21 testEndpoint = "offline@daemon:2222"
22)
23
24// testPolicy is one fully valid, exact policy for the offline sandbox.
25func testPolicy() ports.BrokerPolicy {
26 return ports.BrokerPolicy{
27 ProtocolVersion: protocol.Version,
28 CatalogSchemaVersion: 3,
29 EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
30 Transport: "unix-mux",
31 Trust: "offline-trust",
32 Launch: "offline-launch",
33 Isolation: "offline-isolation",
34 }
35}
36
37// registrationDocument renders one registration with the supplied overrides.
38func registrationDocument(overrides map[string]any) map[string]any {
39 entry := map[string]any{
40 "endpoint": testEndpoint,
41 "incarnation": testIncarnation,
42 "generation": 1,
43 "identity": "offline-daemon",
44 "route": "/tmp/vev-brokerconfig-test/mux.sock",
45 "policy": map[string]any{
46 "protocolVersion": testPolicy().ProtocolVersion,
47 "catalogSchemaVersion": testPolicy().CatalogSchemaVersion,
48 "environmentPolicy": "client-owned",
49 "transport": testPolicy().Transport,
50 "trust": testPolicy().Trust,
51 "launch": testPolicy().Launch,
52 "isolation": testPolicy().Isolation,
53 },
54 }
55 for key, value := range overrides {
56 entry[key] = value
57 }
58 return map[string]any{
59 "marker": Marker,
60 "registrations": []any{entry},
61 }
62}
63
64// writeConfig marshals one document into root/config.json with owner-only
65// permissions.
66func writeConfig(t *testing.T, root string, document any, perm os.FileMode) {
67 t.Helper()
68 raw, err := json.Marshal(document)
69 require.NoError(t, err)
70 require.NoError(t, os.WriteFile(filepath.Join(root, ConfigFileName), raw, perm))
71}
72
73func TestResolveLayout(t *testing.T) {
74 base := t.TempDir()
75 linkParent := filepath.Join(t.TempDir(), "link")
76 require.NoError(t, os.Symlink(base, linkParent))
77
78 tests := []struct {
79 name string
80 root string
81 reserved []string
82 wantErr string
83 }{
84 {name: "valid absolute clean root", root: filepath.Join(base, "sandbox")},
85 {name: "empty root", root: "", wantErr: "empty"},
86 {name: "relative root", root: "sandbox", wantErr: "not absolute"},
87 {name: "unclean root", root: base + "/sandbox/../sandbox", wantErr: "not clean"},
88 {name: "filesystem root", root: string(filepath.Separator), wantErr: "filesystem root"},
89 {name: "symlinked component", root: filepath.Join(linkParent, "sandbox"), wantErr: "symlink"},
90 {name: "reserved equals root", root: filepath.Join(base, "sandbox"), reserved: []string{filepath.Join(base, "sandbox")}, wantErr: "overlaps"},
91 {name: "reserved is parent", root: filepath.Join(base, "sandbox"), reserved: []string{base}, wantErr: "overlaps"},
92 {name: "reserved is child", root: filepath.Join(base, "sandbox"), reserved: []string{filepath.Join(base, "sandbox", "state")}, wantErr: "overlaps"},
93 {name: "reserved relative", root: filepath.Join(base, "sandbox"), reserved: []string{"relative"}, wantErr: "not absolute"},
94 {name: "unrelated reserved is allowed", root: filepath.Join(base, "sandbox"), reserved: []string{filepath.Join(base, "other")}},
95 }
96 for _, tt := range tests {
97 t.Run(tt.name, func(t *testing.T) {
98 layout, err := ResolveLayout(tt.root, tt.reserved)
99 if tt.wantErr != "" {
100 require.Error(t, err)
101 require.Contains(t, err.Error(), tt.wantErr)
102 return
103 }
104 require.NoError(t, err)
105 require.Equal(t, layout.Root, tt.root)
106 require.Equal(t, filepath.Join(tt.root, RuntimeDirName), layout.Runtime)
107 require.Equal(t, filepath.Join(tt.root, StateDirName), layout.State)
108 require.Equal(t, filepath.Join(tt.root, LogDirName), layout.Log)
109 })
110 }
111}
112
113func TestLoadValidConfig(t *testing.T) {
114 root := t.TempDir()
115 writeConfig(t, root, registrationDocument(nil), 0o600)
116 layout, err := ResolveLayout(root, nil)
117 require.NoError(t, err)
118
119 config, err := Load(layout)
120 require.NoError(t, err)
121 require.Equal(t, []string{testEndpoint}, config.Endpoints())
122
123 resolved, err := config.Resolver().ResolveDialTarget(context.Background(), ports.BrokerOpenStreamRequest{
124 Purpose: ports.BrokerStreamControl,
125 Endpoint: testEndpoint,
126 Registration: testRegistration(),
127 Policy: testPolicy(),
128 StartMode: ports.BrokerDaemonExistingOnly,
129 })
130 require.NoError(t, err)
131 require.Equal(t, ports.BrokerDaemonIdentity("offline-daemon"), resolved.ExpectedIdentity.Identity)
132 require.Equal(t, testPolicy(), resolved.Policy)
133 require.Equal(t, ports.BrokerDaemonExistingOnly, resolved.StartMode, "resolution propagates the requested start mode")
134
135 local, err := config.LocalMuxRoute()
136 require.NoError(t, err)
137 require.Equal(t, RouteUnix, local.Kind())
138 require.Equal(t, "/tmp/vev-brokerconfig-test/mux.sock", local.Path())
139 require.Equal(t, local.Address(), resolved.Address)
140 require.LessOrEqual(t, len(resolved.Address), MaxRouteAddressBytes)
141 route, ok := config.RouteByAddress(resolved.Address)
142 require.True(t, ok)
143 require.Equal(t, local.Path(), route.Path())
144 _, ok = config.RouteByAddress("offline-route-unknown")
145 require.False(t, ok)
146}
147
148func TestLoadAcceptsEmptyRegistrations(t *testing.T) {
149 root := t.TempDir()
150 writeConfig(t, root, map[string]any{"marker": Marker, "registrations": []any{}}, 0o600)
151 layout, err := ResolveLayout(root, nil)
152 require.NoError(t, err)
153 config, err := Load(layout)
154 require.NoError(t, err)
155 require.Empty(t, config.Endpoints())
156}
157
158func TestLoadRejects(t *testing.T) {
159 tests := []struct {
160 name string
161 build func(t *testing.T, root string)
162 wantErr string
163 }{
164 {
165 name: "missing file",
166 build: func(*testing.T, string) {},
167 wantErr: "open",
168 },
169 {
170 name: "wrong marker",
171 build: func(t *testing.T, root string) {
172 writeConfig(t, root, map[string]any{"marker": "other", "registrations": []any{}}, 0o600)
173 },
174 wantErr: "marker",
175 },
176 {
177 name: "unknown field",
178 build: func(t *testing.T, root string) {
179 document := registrationDocument(nil)
180 document["extra"] = true
181 writeConfig(t, root, document, 0o600)
182 },
183 wantErr: "unknown field",
184 },
185 {
186 name: "duplicate key",
187 build: func(t *testing.T, root string) {
188 writeRawConfig(t, root, `{"marker":"`+Marker+`","marker":"`+Marker+`"}`)
189 },
190 wantErr: "duplicate JSON key",
191 },
192 {
193 name: "trailing json",
194 build: func(t *testing.T, root string) {
195 writeRawConfig(t, root, `{"marker":"`+Marker+`","registrations":[]} {}`)
196 },
197 wantErr: "trailing JSON",
198 },
199 {
200 name: "invalid utf8",
201 build: func(t *testing.T, root string) { writeRawConfig(t, root, "{\"marker\":\"\xff\"}") },
202 wantErr: "encoding",
203 },
204 {
205 name: "group readable",
206 build: func(t *testing.T, root string) { writeConfig(t, root, registrationDocument(nil), 0o640) },
207 wantErr: "owner-only",
208 },
209 {
210 name: "symlinked config",
211 build: func(t *testing.T, root string) {
212 target := filepath.Join(t.TempDir(), "real.json")
213 raw, err := json.Marshal(registrationDocument(nil))
214 require.NoError(t, err)
215 require.NoError(t, os.WriteFile(target, raw, 0o600))
216 require.NoError(t, os.Symlink(target, filepath.Join(root, ConfigFileName)))
217 },
218 wantErr: "symlink",
219 },
220 {
221 name: "nonregular config",
222 build: func(t *testing.T, root string) {
223 require.NoError(t, os.Mkdir(filepath.Join(root, ConfigFileName), 0o700))
224 },
225 wantErr: "not a regular file",
226 },
227 {
228 name: "oversized config",
229 build: func(t *testing.T, root string) {
230 require.NoError(t, os.WriteFile(filepath.Join(root, ConfigFileName), make([]byte, MaxConfigBytes+1), 0o600))
231 },
232 wantErr: "exceeds",
233 },
234 {
235 name: "symlinked route",
236 build: func(t *testing.T, root string) {
237 link := filepath.Join(t.TempDir(), "route-link")
238 require.NoError(t, os.Symlink(t.TempDir(), link))
239 writeConfig(t, root, registrationDocument(map[string]any{"route": filepath.Join(link, "mux.sock")}), 0o600)
240 },
241 wantErr: "symlink",
242 },
243 {
244 name: "bad endpoint",
245 build: func(t *testing.T, root string) {
246 writeConfig(t, root, registrationDocument(map[string]any{"endpoint": "bad host"}), 0o600)
247 },
248 wantErr: "whitespace",
249 },
250 {
251 name: "incarnation length",
252 build: func(t *testing.T, root string) {
253 writeConfig(t, root, registrationDocument(map[string]any{"incarnation": "abcd"}), 0o600)
254 },
255 wantErr: "32 hexadecimal",
256 },
257 {
258 name: "incarnation zero",
259 build: func(t *testing.T, root string) {
260 writeConfig(t, root, registrationDocument(map[string]any{"incarnation": strings.Repeat("0", 32)}), 0o600)
261 },
262 wantErr: "no incarnation",
263 },
264 {
265 name: "generation zero",
266 build: func(t *testing.T, root string) {
267 writeConfig(t, root, registrationDocument(map[string]any{"generation": 0}), 0o600)
268 },
269 wantErr: "no generation",
270 },
271 {
272 name: "empty identity",
273 build: func(t *testing.T, root string) {
274 writeConfig(t, root, registrationDocument(map[string]any{"identity": ""}), 0o600)
275 },
276 wantErr: "identity",
277 },
278 {
279 name: "bad environment policy",
280 build: func(t *testing.T, root string) {
281 writeConfig(t, root, documentWithPolicy(t, "environmentPolicy", "shared"), 0o600)
282 },
283 wantErr: "environmentPolicy",
284 },
285 {
286 name: "zero protocol version",
287 build: func(t *testing.T, root string) {
288 writeConfig(t, root, documentWithPolicy(t, "protocolVersion", 0), 0o600)
289 },
290 wantErr: "no protocol version",
291 },
292 {
293 name: "relative route",
294 build: func(t *testing.T, root string) {
295 writeConfig(t, root, registrationDocument(map[string]any{"route": "mux.sock"}), 0o600)
296 },
297 wantErr: "not absolute",
298 },
299 {
300 name: "duplicate endpoint",
301 build: func(t *testing.T, root string) {
302 document := registrationDocument(nil)
303 registrations := document["registrations"].([]any)
304 document["registrations"] = append(registrations, registrations[0])
305 writeConfig(t, root, document, 0o600)
306 },
307 wantErr: "duplicate endpoint",
308 },
309 }
310 for _, tt := range tests {
311 t.Run(tt.name, func(t *testing.T) {
312 root := t.TempDir()
313 tt.build(t, root)
314 layout, err := ResolveLayout(root, nil)
315 require.NoError(t, err)
316 _, err = Load(layout)
317 require.Error(t, err)
318 require.Contains(t, err.Error(), tt.wantErr)
319 })
320 }
321}
322
323func TestLoadRejectsRouteOverlappingProduction(t *testing.T) {
324 prodRuntime := t.TempDir()
325 prodState := t.TempDir()
326
327 tests := []struct {
328 name string
329 route string
330 wantErr bool
331 }{
332 {name: "route under production runtime", route: filepath.Join(prodRuntime, "broker.sock"), wantErr: true},
333 {name: "route equals production state", route: prodState, wantErr: true},
334 {name: "route contains production runtime", route: filepath.Dir(prodRuntime), wantErr: true},
335 {name: "unrelated route", route: filepath.Join(t.TempDir(), "mux.sock")},
336 }
337 for _, tt := range tests {
338 t.Run(tt.name, func(t *testing.T) {
339 root := t.TempDir()
340 writeConfig(t, root, registrationDocument(map[string]any{"route": tt.route}), 0o600)
341 layout, err := ResolveLayout(root, []string{prodRuntime, prodState})
342 require.NoError(t, err)
343 _, err = Load(layout)
344 if !tt.wantErr {
345 require.NoError(t, err)
346 return
347 }
348 require.Error(t, err)
349 require.Contains(t, err.Error(), "overlaps production path")
350 })
351 }
352}
353
354func TestLayoutVerifyCreatedDetectsSymlinkedComponent(t *testing.T) {
355 root := filepath.Join(t.TempDir(), "sandbox")
356 layout, err := ResolveLayout(root, nil)
357 require.NoError(t, err)
358
359 // Nothing is created yet, so the walk ends at the first missing component.
360 require.NoError(t, layout.VerifyCreated())
361
362 require.NoError(t, os.MkdirAll(root, 0o700))
363 require.NoError(t, os.Symlink(t.TempDir(), layout.State))
364 require.ErrorContains(t, layout.VerifyCreated(), "symlink")
365
366 // Once the swap is removed, every created component is symlink-free again.
367 require.NoError(t, os.Remove(layout.State))
368 require.NoError(t, layout.VerifyCreated())
369}
370
371// registrationsDocument renders one document carrying the supplied registration
372// entries in order.
373func registrationsDocument(entries ...map[string]any) map[string]any {
374 list := make([]any, 0, len(entries))
375 for _, entry := range entries {
376 list = append(list, entry)
377 }
378 return map[string]any{"marker": Marker, "registrations": list}
379}
380
381// registrationEntry renders one registration entry with the supplied overrides.
382func registrationEntry(overrides map[string]any) map[string]any {
383 return registrationDocument(overrides)["registrations"].([]any)[0].(map[string]any)
384}
385
386// TestLoadEnforcesOneRoutePerPooledIdentity pins the pooling invariant: the
387// broker pool keys one physical transport by the exact authenticated identity
388// plus policy pair and deliberately ignores the opaque route address, so two
389// registrations that share the pair must resolve to the same route. An alias on
390// an identical route is accepted; a second route for the same pair is refused,
391// because the pool would otherwise dial one of the two addresses and silently
392// strand the other.
393func TestLoadEnforcesOneRoutePerPooledIdentity(t *testing.T) {
394 const (
395 aliasOne = "alias@daemon:2222"
396 aliasTwo = "alias@daemon:2223"
397 otherID = "other@daemon:2224"
398 routeOne = "/tmp/vev-brokerconfig-test/one.sock"
399 routeTwo = "/tmp/vev-brokerconfig-test/two.sock"
400 )
401 withPolicy := func(entry map[string]any, key string, value any) map[string]any {
402 entry["policy"].(map[string]any)[key] = value
403 return entry
404 }
405 tests := []struct {
406 name string
407 entries []map[string]any
408 wantErr string
409 wantRoutes []string
410 }{
411 {
412 name: "same identity and policy on a different route is refused",
413 entries: []map[string]any{
414 registrationEntry(map[string]any{"endpoint": aliasOne, "route": routeOne}),
415 registrationEntry(map[string]any{"endpoint": aliasTwo, "route": routeTwo}),
416 },
417 wantErr: "different route",
418 },
419 {
420 name: "same identity and policy on the identical route is an alias",
421 entries: []map[string]any{
422 registrationEntry(map[string]any{"endpoint": aliasOne, "route": routeOne}),
423 registrationEntry(map[string]any{"endpoint": aliasTwo, "route": routeOne}),
424 },
425 wantRoutes: []string{aliasOne, aliasTwo},
426 },
427 {
428 name: "distinct identities on different routes are independent",
429 entries: []map[string]any{
430 registrationEntry(map[string]any{"endpoint": aliasOne, "route": routeOne}),
431 registrationEntry(map[string]any{"endpoint": otherID, "route": routeTwo, "identity": "other-daemon"}),
432 },
433 wantRoutes: []string{aliasOne, otherID},
434 },
435 {
436 name: "same identity with a distinct policy on a different route is independent",
437 entries: []map[string]any{
438 registrationEntry(map[string]any{"endpoint": aliasOne, "route": routeOne}),
439 withPolicy(registrationEntry(map[string]any{"endpoint": aliasTwo, "route": routeTwo}), "transport", "other-transport"),
440 },
441 wantRoutes: []string{aliasOne, aliasTwo},
442 },
443 }
444 for _, tt := range tests {
445 t.Run(tt.name, func(t *testing.T) {
446 root := t.TempDir()
447 writeConfig(t, root, registrationsDocument(tt.entries...), 0o600)
448 layout, err := ResolveLayout(root, nil)
449 require.NoError(t, err)
450 config, err := Load(layout)
451 if tt.wantErr != "" {
452 require.Error(t, err)
453 require.Contains(t, err.Error(), tt.wantErr)
454 return
455 }
456 require.NoError(t, err)
457 require.Equal(t, tt.wantRoutes, config.Endpoints())
458 })
459 }
460}
461
462// TestLoadAliasesShareOneRoute proves an accepted alias resolves both endpoints
463// to the identical opaque address and that the shared local route is still
464// reported exactly once, so a remote-side helper is never handed a duplicate of
465// the same carriage.
466func TestLoadAliasesShareOneRoute(t *testing.T) {
467 const (
468 first = "alias@daemon:2222"
469 second = "alias@daemon:2223"
470 route = "/tmp/vev-brokerconfig-test/shared.sock"
471 )
472 root := t.TempDir()
473 writeConfig(t, root, registrationsDocument(
474 registrationEntry(map[string]any{"endpoint": first, "route": route}),
475 registrationEntry(map[string]any{"endpoint": second, "route": route}),
476 ), 0o600)
477 layout, err := ResolveLayout(root, nil)
478 require.NoError(t, err)
479 config, err := Load(layout)
480 require.NoError(t, err)
481
482 local, err := config.LocalMuxRoute()
483 require.NoError(t, err)
484 require.Equal(t, route, local.Path())
485
486 for _, endpoint := range []string{first, second} {
487 registration := testRegistration()
488 registration.Endpoint = endpoint
489 resolved, err := config.Resolver().ResolveDialTarget(context.Background(), ports.BrokerOpenStreamRequest{
490 Purpose: ports.BrokerStreamControl,
491 Endpoint: endpoint,
492 Registration: registration,
493 Policy: testPolicy(),
494 StartMode: ports.BrokerDaemonStartIfNeeded,
495 })
496 require.NoError(t, err)
497 require.Equal(t, local.Address(), resolved.Address)
498 require.Equal(t, ports.BrokerDaemonStartIfNeeded, resolved.StartMode)
499 }
500}
501
502// documentWithPolicy renders a valid document with one policy field overridden.
503func documentWithPolicy(t *testing.T, key string, value any) map[string]any {
504 t.Helper()
505 document := registrationDocument(nil)
506 entry := document["registrations"].([]any)[0].(map[string]any)
507 entry["policy"].(map[string]any)[key] = value
508 return document
509}
510
511// writeRawConfig writes one raw config file with owner-only permissions.
512func writeRawConfig(t *testing.T, root, content string) {
513 t.Helper()
514 require.NoError(t, os.WriteFile(filepath.Join(root, ConfigFileName), []byte(content), 0o600))
515}
516
517// testRegistration is the exact registration the fixture document provisions.
518func testRegistration() domain.RemoteRegistration {
519 var registration domain.RemoteRegistration
520 registration.Endpoint = testEndpoint
521 for i := range registration.Incarnation {
522 registration.Incarnation[i] = byte(i + 1)
523 }
524 registration.Generation = 1
525 return registration
526}
527
528func TestResolverRefusals(t *testing.T) {
529 root := t.TempDir()
530 writeConfig(t, root, registrationDocument(nil), 0o600)
531 layout, err := ResolveLayout(root, nil)
532 require.NoError(t, err)
533 config, err := Load(layout)
534 require.NoError(t, err)
535 resolver := config.Resolver()
536
537 base := ports.BrokerOpenStreamRequest{
538 Purpose: ports.BrokerStreamControl,
539 Endpoint: testEndpoint,
540 Registration: testRegistration(),
541 Policy: testPolicy(),
542 StartMode: ports.BrokerDaemonExistingOnly,
543 }
544
545 t.Run("unknown endpoint", func(t *testing.T) {
546 request := base
547 request.Endpoint = "other@daemon"
548 _, err := resolver.ResolveDialTarget(context.Background(), request)
549 var typed ports.BrokerError
550 require.ErrorAs(t, err, &typed)
551 require.Equal(t, ports.BrokerErrorUnavailable, typed.Code)
552 require.ErrorIs(t, err, errUnknownEndpoint)
553 })
554 t.Run("local request", func(t *testing.T) {
555 request := base
556 request.Local = true
557 _, err := resolver.ResolveDialTarget(context.Background(), request)
558 var typed ports.BrokerError
559 require.ErrorAs(t, err, &typed)
560 require.Equal(t, ports.BrokerErrorUnavailable, typed.Code)
561 require.ErrorIs(t, err, errNoLocalRoute)
562 })
563 t.Run("stale registration", func(t *testing.T) {
564 request := base
565 request.Registration.Generation = 2
566 _, err := resolver.ResolveDialTarget(context.Background(), request)
567 var typed ports.BrokerError
568 require.ErrorAs(t, err, &typed)
569 require.Equal(t, ports.BrokerErrorUnavailable, typed.Code)
570 require.ErrorIs(t, err, errStaleRegistration)
571 })
572 t.Run("conflicting policy", func(t *testing.T) {
573 request := base
574 request.Policy.Transport = "other-transport"
575 _, err := resolver.ResolveDialTarget(context.Background(), request)
576 var typed ports.BrokerError
577 require.ErrorAs(t, err, &typed)
578 require.Equal(t, ports.BrokerErrorConflictingPolicy, typed.Code)
579 })
580 t.Run("cancelled context", func(t *testing.T) {
581 ctx, cancel := context.WithCancel(context.Background())
582 cancel()
583 _, err := resolver.ResolveDialTarget(ctx, base)
584 require.ErrorIs(t, err, context.Canceled)
585 })
586}
587
588func TestDecodeStrictRejectsNesting(t *testing.T) {
589 root := t.TempDir()
590 nested := strings.Repeat("[", 130) + strings.Repeat("]", 130)
591 writeRawConfig(t, root, `{"marker":"`+Marker+`","registrations":`+nested+`}`)
592 layout, err := ResolveLayout(root, nil)
593 require.NoError(t, err)
594 _, err = Load(layout)
595 require.Error(t, err)
596 require.Contains(t, err.Error(), "nesting")
597}
598
599func TestResolveLayoutDoesNotCreatePaths(t *testing.T) {
600 root := filepath.Join(t.TempDir(), "does-not-exist")
601 layout, err := ResolveLayout(root, nil)
602 require.NoError(t, err)
603 require.Equal(t, filepath.Join(root, StateDirName), layout.State)
604 require.Equal(t, filepath.Join(root, RuntimeDirName, SpawnDirName), layout.Spawn)
605 _, statErr := os.Lstat(root)
606 require.ErrorIs(t, statErr, os.ErrNotExist)
607}
608
609func TestLoadIdleGrace(t *testing.T) {
610 tests := []struct {
611 name string
612 document map[string]any
613 want time.Duration
614 wantSet bool
615 wantErr string
616 }{
617 {
618 name: "provisioned grace is parsed",
619 document: map[string]any{"marker": Marker, "registrations": []any{}, "idleGrace": "90s"},
620 want: 90 * time.Second,
621 wantSet: true,
622 },
623 {name: "absent grace provisions nothing", document: map[string]any{"marker": Marker, "registrations": []any{}}},
624 {
625 name: "invalid duration is refused",
626 document: map[string]any{"marker": Marker, "registrations": []any{}, "idleGrace": "soon"},
627 wantErr: "not a duration",
628 },
629 {
630 name: "non-positive grace is refused",
631 document: map[string]any{"marker": Marker, "registrations": []any{}, "idleGrace": "0s"},
632 wantErr: "must be positive",
633 },
634 {
635 name: "oversized grace is refused",
636 document: map[string]any{"marker": Marker, "registrations": []any{}, "idleGrace": (MaxIdleGrace + time.Second).String()},
637 wantErr: "must be positive and at most",
638 },
639 }
640 for _, tt := range tests {
641 t.Run(tt.name, func(t *testing.T) {
642 root := t.TempDir()
643 writeConfig(t, root, tt.document, 0o600)
644 layout, err := ResolveLayout(root, nil)
645 require.NoError(t, err)
646 config, err := Load(layout)
647 if tt.wantErr != "" {
648 require.Error(t, err)
649 require.Contains(t, err.Error(), tt.wantErr)
650 return
651 }
652 require.NoError(t, err)
653 grace, set := config.IdleGrace()
654 require.Equal(t, tt.wantSet, set)
655 require.Equal(t, tt.want, grace)
656 })
657 }
658}
659
660func TestLoadWarmTransports(t *testing.T) {
661 document := func(extra map[string]any) map[string]any {
662 d := map[string]any{"marker": Marker, "registrations": []any{}}
663 for k, v := range extra {
664 d[k] = v
665 }
666 return d
667 }
668 tests := []struct {
669 name string
670 document map[string]any
671 wantCount int
672 wantTimeout time.Duration
673 wantErr string
674 }{
675 {name: "defaults match the old client cache", document: document(nil), wantCount: DefaultWarmTransports},
676 {name: "explicit off", document: document(map[string]any{"warmIdleTimeout": "off"}), wantCount: DefaultWarmTransports},
677 {name: "bounded count and grace", document: document(map[string]any{"warmTransports": 2, "warmIdleTimeout": "5m"}), wantCount: 2, wantTimeout: 5 * time.Minute},
678 {name: "zero disables warm reuse", document: document(map[string]any{"warmTransports": 0}), wantCount: 0},
679 {name: "maximum count", document: document(map[string]any{"warmTransports": MaxWarmTransports}), wantCount: MaxWarmTransports},
680 {name: "negative count", document: document(map[string]any{"warmTransports": -1}), wantErr: "warmTransports must be between"},
681 {name: "oversized count", document: document(map[string]any{"warmTransports": MaxWarmTransports + 1}), wantErr: "warmTransports must be between"},
682 {name: "non-integer count", document: document(map[string]any{"warmTransports": "8"}), wantErr: "warmTransports"},
683 {name: "invalid timeout", document: document(map[string]any{"warmIdleTimeout": "soon"}), wantErr: "warmIdleTimeout"},
684 {name: "zero timeout", document: document(map[string]any{"warmIdleTimeout": "0s"}), wantErr: "warmIdleTimeout"},
685 {name: "oversized timeout", document: document(map[string]any{"warmIdleTimeout": (MaxIdleGrace + time.Second).String()}), wantErr: "warmIdleTimeout"},
686 }
687 for _, tt := range tests {
688 t.Run(tt.name, func(t *testing.T) {
689 root := t.TempDir()
690 writeConfig(t, root, tt.document, 0o600)
691 layout, err := ResolveLayout(root, nil)
692 require.NoError(t, err)
693 config, err := Load(layout)
694 if tt.wantErr != "" {
695 require.ErrorContains(t, err, tt.wantErr)
696 return
697 }
698 require.NoError(t, err)
699 require.Equal(t, tt.wantCount, config.WarmTransports())
700 require.Equal(t, tt.wantTimeout, config.WarmIdleTimeout())
701 })
702 }
703 var none *Config
704 require.Equal(t, DefaultWarmTransports, none.WarmTransports())
705 require.Zero(t, none.WarmIdleTimeout())
706}
707
708func TestRouteValidateDerivedAuthority(t *testing.T) {
709 unix, err := newUnixRoute("/tmp/vev-brokerconfig-test/mux.sock")
710 require.NoError(t, err)
711 ssh, err := newSSHRoute(RouteSSHStdio, "host", []string{"vev"}, TrustInputs{})
712 require.NoError(t, err)
713 tests := []struct {
714 name string
715 route Route
716 valid bool
717 }{
718 {"unix", unix, true},
719 {"ssh", ssh, true},
720 {"missing address", Route{kind: RouteUnix, path: unix.path, local: true}, false},
721 {"wrong address", Route{kind: RouteUnix, path: unix.path, local: true, address: ssh.address}, false},
722 {"unix not local", Route{kind: RouteUnix, path: unix.path, address: unix.address}, false},
723 {"ssh local", Route{kind: ssh.kind, target: ssh.target, argv: ssh.argv, address: ssh.address, local: true}, false},
724 }
725 for _, tt := range tests {
726 t.Run(tt.name, func(t *testing.T) {
727 err := tt.route.Validate()
728 if tt.valid {
729 require.NoError(t, err)
730 } else {
731 require.Error(t, err)
732 }
733 })
734 }
735}
736
737func TestResolverDoesNotAliasConfiguration(t *testing.T) {
738 config, err := loadDocument(t, documentWithLocal(t, localTestDocument()), nil)
739 require.NoError(t, err)
740 resolver := config.Resolver()
741 original := config.Resolver()
742 registration := resolver.byEndpoint[testEndpoint]
743 registration.Route.argv = []string{"changed"}
744 resolver.byEndpoint[testEndpoint] = registration
745 delete(resolver.byEndpoint, testEndpoint)
746 require.Equal(t, original.byEndpoint[testEndpoint], config.Resolver().byEndpoint[testEndpoint])
747 require.NotNil(t, resolver.local)
748 resolver.local.Identity = "changed"
749 require.Equal(t, original.local.Identity, config.Resolver().local.Identity)
750}
751
752func TestRejectSymlinkedComponentsAllowsRootOwnedSystemLinks(t *testing.T) {
753 var rootLink string
754 for _, candidate := range []string{"/var", "/tmp", "/bin", "/lib", "/sbin"} {
755 info, err := os.Lstat(candidate)
756 if err == nil && info.Mode()&os.ModeSymlink != 0 && ownedByRoot(info) {
757 rootLink = candidate
758 break
759 }
760 }
761 if rootLink == "" {
762 t.Skip("no root-owned top-level symlink on this system")
763 }
764 require.NoError(t, rejectSymlinkedComponents(filepath.Join(rootLink, "vev-missing-child")))
765}