config_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close