v0.8.1 1f54f61

route_test.go RawHistoryFolder

1package brokerconfig
2
3import (
4	"encoding/json"
5	"strings"
6	"testing"
7	"time"
8
9	"github.com/stretchr/testify/require"
10)
11
12// TestParseRouteVariants pins the explicit bounded route union: a bare string
13// is the original Unix path form, an object carries an explicit kind, and every
14// kind/field mismatch or bound violation is refused.
15func TestParseRouteVariants(t *testing.T) {
16	t.Parallel()
17
18	tests := []struct {
19		name    string
20		raw     string
21		wantErr string
22		check   func(t *testing.T, route Route)
23	}{
24		{
25			name: "bare string is a Unix route",
26			raw:  `"/tmp/mux.sock"`,
27			check: func(t *testing.T, route Route) {
28				require.Equal(t, RouteUnix, route.Kind())
29				require.Equal(t, "/tmp/mux.sock", route.Path())
30				require.True(t, route.IsLocal())
31			},
32		},
33		{
34			name: "explicit unix route",
35			raw:  `{"kind":"unix","path":"/tmp/mux.sock"}`,
36			check: func(t *testing.T, route Route) {
37				require.Equal(t, RouteUnix, route.Kind())
38				require.Equal(t, "/tmp/mux.sock", route.Path())
39			},
40		},
41		{
42			name: "ssh stdio route with trust inputs",
43			raw: `{"kind":"ssh-stdio","target":"user@host:2222","argv":["vev","_broker-mux-stdio","--offline-root","/srv/vev"],` +
44				`"trust":{"knownHostsFile":"/etc/ssh/known_hosts","connectTimeout":"30s"}}`,
45			check: func(t *testing.T, route Route) {
46				require.Equal(t, RouteSSHStdio, route.Kind())
47				require.Equal(t, "user@host:2222", route.Target())
48				require.Equal(t, []string{"vev", "_broker-mux-stdio", "--offline-root", "/srv/vev"}, route.Argv())
49				require.Equal(t, "/etc/ssh/known_hosts", route.KnownHostsFile())
50				require.Equal(t, 30*time.Second, route.ConnectTimeout())
51				require.False(t, route.IsLocal())
52			},
53		},
54		{
55			name: "ssh quic route without trust",
56			raw:  `{"kind":"ssh-quic","target":"host","argv":["vev","_broker-mux-quic-bootstrap","--offline-root","/srv/vev"]}`,
57			check: func(t *testing.T, route Route) {
58				require.Equal(t, RouteSSHQUIC, route.Kind())
59				require.Equal(t, "host", route.Target())
60				require.Empty(t, route.KnownHostsFile())
61				require.Zero(t, route.ConnectTimeout())
62			},
63		},
64		{name: "empty route", raw: ``, wantErr: "empty"},
65		{name: "null route", raw: `null`, wantErr: "path string or an object"},
66		{name: "number route", raw: `42`, wantErr: "path string or an object"},
67		{name: "unknown kind", raw: `{"kind":"carrier-pigeon","target":"h","argv":["x"]}`, wantErr: "invalid"},
68		{name: "missing kind", raw: `{"path":"/tmp/mux.sock"}`, wantErr: "requires a kind"},
69		{name: "unknown field", raw: `{"kind":"unix","path":"/tmp/mux.sock","extra":1}`, wantErr: "unknown field"},
70		{name: "trailing json", raw: `{"kind":"unix","path":"/tmp/mux.sock"} {}`, wantErr: "trailing"},
71		{name: "unix carries ssh fields", raw: `{"kind":"unix","path":"/tmp/mux.sock","target":"h"}`, wantErr: "must not carry ssh"},
72		{name: "ssh without argv", raw: `{"kind":"ssh-stdio","target":"h"}`, wantErr: "argv"},
73		{name: "ssh empty argv word", raw: `{"kind":"ssh-stdio","target":"h","argv":["vev",""]}`, wantErr: "empty"},
74		{name: "ssh word control char", raw: `{"kind":"ssh-stdio","target":"h","argv":["vev","a\nb"]}`, wantErr: "control"},
75		{name: "ssh target whitespace", raw: `{"kind":"ssh-stdio","target":"a b","argv":["vev"]}`, wantErr: "whitespace"},
76		{name: "ssh target leading dash", raw: `{"kind":"ssh-stdio","target":"-oProxyCommand=x","argv":["vev"]}`, wantErr: "dash"},
77		{name: "ssh target too long", raw: `{"kind":"ssh-stdio","target":"` + strings.Repeat("a", MaxRouteTargetBytes+1) + `","argv":["vev"]}`, wantErr: "exceeds"},
78		{name: "too many argv words", raw: `{"kind":"ssh-stdio","target":"h","argv":[` + strings.Repeat(`"w",`, MaxRouteArgvWords) + `"w"]}`, wantErr: "words"},
79		{name: "relative trust path", raw: `{"kind":"ssh-stdio","target":"h","argv":["vev"],"trust":{"knownHostsFile":"known_hosts"}}`, wantErr: "not absolute"},
80		{name: "unclean trust path", raw: `{"kind":"ssh-stdio","target":"h","argv":["vev"],"trust":{"knownHostsFile":"/etc/ssh/../known_hosts"}}`, wantErr: "not clean"},
81		{name: "space in trust path", raw: `{"kind":"ssh-stdio","target":"h","argv":["vev"],"trust":{"knownHostsFile":"/etc/ssh/known hosts"}}`, wantErr: "whitespace"},
82		{name: "bad timeout duration", raw: `{"kind":"ssh-stdio","target":"h","argv":["vev"],"trust":{"connectTimeout":"soon"}}`, wantErr: "not a duration"},
83		{name: "negative timeout", raw: `{"kind":"ssh-stdio","target":"h","argv":["vev"],"trust":{"connectTimeout":"-1s"}}`, wantErr: "positive"},
84		{name: "excessive timeout", raw: `{"kind":"ssh-stdio","target":"h","argv":["vev"],"trust":{"connectTimeout":"1h"}}`, wantErr: "between"},
85	}
86	for _, tt := range tests {
87		t.Run(tt.name, func(t *testing.T) {
88			route, err := parseRoute(json.RawMessage(tt.raw))
89			if tt.wantErr != "" {
90				require.Error(t, err)
91				require.Contains(t, err.Error(), tt.wantErr)
92				return
93			}
94			require.NoError(t, err)
95			require.NoError(t, route.Validate())
96			if tt.check != nil {
97				tt.check(t, route)
98			}
99		})
100	}
101}
102
103// TestRouteAddressIsOpaqueAndDeterministic proves the pool address leaks no
104// route detail, is bounded, is stable across parses, and distinguishes distinct
105// routes.
106func TestRouteAddressIsOpaqueAndDeterministic(t *testing.T) {
107	t.Parallel()
108
109	unix, err := parseRoute(json.RawMessage(`{"kind":"unix","path":"/tmp/secret-dir/mux.sock"}`))
110	require.NoError(t, err)
111	stdio, err := parseRoute(json.RawMessage(`{"kind":"ssh-stdio","target":"user@secret-host","argv":["vev","_broker-mux-stdio","--offline-root","/srv/secret"]}`))
112	require.NoError(t, err)
113	quic, err := parseRoute(json.RawMessage(`{"kind":"ssh-quic","target":"user@secret-host","argv":["vev","_broker-mux-quic-bootstrap","--offline-root","/srv/secret"]}`))
114	require.NoError(t, err)
115
116	require.NotEmpty(t, unix.Address())
117	require.LessOrEqual(t, len(unix.Address()), MaxRouteAddressBytes)
118	require.Contains(t, unix.Address(), "offline-route-")
119
120	for _, secret := range []string{"secret-host", "secret-dir", "/srv/secret", "_broker-mux", "mux.sock"} {
121		for _, route := range []Route{unix, stdio, quic} {
122			require.NotContains(t, route.Address(), secret, "address must not leak route detail")
123		}
124	}
125	require.NotEqual(t, unix.Address(), stdio.Address())
126	require.NotEqual(t, stdio.Address(), quic.Address())
127
128	again, err := parseRoute(json.RawMessage(`{"kind":"ssh-stdio","target":"user@secret-host","argv":["vev","_broker-mux-stdio","--offline-root","/srv/secret"]}`))
129	require.NoError(t, err)
130	require.Equal(t, stdio.Address(), again.Address(), "address must be stable across parses")
131
132	// A different trust input is a different route and therefore a different
133	// address.
134	trusted, err := parseRoute(json.RawMessage(`{"kind":"ssh-stdio","target":"user@secret-host","argv":["vev","_broker-mux-stdio","--offline-root","/srv/secret"],"trust":{"connectTimeout":"5s"}}`))
135	require.NoError(t, err)
136	require.NotEqual(t, stdio.Address(), trusted.Address())
137}
138
139// TestConfigLocalMuxRouteRules proves a helper only ever bridges to exactly one
140// provisioned Unix route.
141func TestConfigLocalMuxRouteRules(t *testing.T) {
142	t.Parallel()
143
144	root := t.TempDir()
145	writeConfig(t, root, emptyRouteConfig(), 0o600)
146	layout, err := ResolveLayout(root, nil)
147	require.NoError(t, err)
148	config, err := Load(layout)
149	require.NoError(t, err)
150	_, err = config.LocalMuxRoute()
151	require.Error(t, err)
152	require.Contains(t, err.Error(), "no Unix daemon-mux route")
153
154	root = t.TempDir()
155	writeConfig(t, root, routeConfig(
156		map[string]any{"kind": "unix", "path": "/tmp/one.sock"},
157		map[string]any{"kind": "unix", "path": "/tmp/two.sock"},
158	), 0o600)
159	layout, err = ResolveLayout(root, nil)
160	require.NoError(t, err)
161	config, err = Load(layout)
162	require.NoError(t, err)
163	_, err = config.LocalMuxRoute()
164	require.Error(t, err)
165	require.Contains(t, err.Error(), "more than one")
166
167	root = t.TempDir()
168	writeConfig(t, root, routeConfig(map[string]any{"kind": "unix", "path": "/tmp/one.sock"}), 0o600)
169	layout, err = ResolveLayout(root, nil)
170	require.NoError(t, err)
171	config, err = Load(layout)
172	require.NoError(t, err)
173	route, err := config.LocalMuxRoute()
174	require.NoError(t, err)
175	require.Equal(t, "/tmp/one.sock", route.Path())
176}
177
178// emptyRouteConfig renders a marker-valid document with no registrations.
179func emptyRouteConfig() map[string]any {
180	return map[string]any{"marker": Marker, "registrations": []any{}}
181}
182
183// routeConfig renders one marker-valid document whose registrations carry the
184// supplied explicit route objects. Each registration gets a distinct endpoint
185// and authenticated identity, so a document with several routes exercises the
186// route rules rather than the one-route-per-pooled-identity rule.
187func routeConfig(routes ...map[string]any) map[string]any {
188	registrations := make([]any, 0, len(routes))
189	for i, route := range routes {
190		registrations = append(registrations, map[string]any{
191			"endpoint":    testEndpoint + "-" + string(rune('a'+i)),
192			"incarnation": "0102030405060708090a0b0c0d0e0f10",
193			"generation":  1,
194			"identity":    "offline-daemon-" + string(rune('a'+i)),
195			"route":       route,
196			"policy": map[string]any{
197				"protocolVersion":      1,
198				"catalogSchemaVersion": 3,
199				"environmentPolicy":    "client-owned",
200				"transport":            "offline",
201				"trust":                "offline-trust",
202				"launch":               "offline-launch",
203				"isolation":            "offline-isolation",
204			},
205		})
206	}
207	return map[string]any{"marker": Marker, "registrations": registrations}
208}

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