main ece7682

broker_store_test.go RawHistoryFolder

1package ports
2
3import (
4	"errors"
5	"strings"
6	"testing"
7
8	"github.com/bnema/vev/internal/domain"
9	"github.com/bnema/vev/internal/protocol"
10	"github.com/bnema/vev/internal/protocol/catalogue"
11	"github.com/stretchr/testify/require"
12)
13
14func TestBrokerSnapshotDurableProjection(t *testing.T) {
15	published := BrokerSnapshot{
16		Epoch: 2, Revision: 3,
17		Removed: []BrokerHostTombstone{{Endpoint: "gone"}},
18		Daemons: []BrokerDaemonObservation{
19			{Local: true, Policy: BrokerPolicy{Transport: "ssh"}},
20			{
21				Endpoint:    "user@host:22",
22				Checking:    true,
23				LastFailure: domain.RemoteFailure{Kind: domain.RemoteFailureTimeout, Err: errors.New("live cause")},
24				Policy:      BrokerPolicy{Transport: "ssh"},
25				Sessions: []catalogue.RemoteCatalogSession{{Name: "work", Tabs: []catalogue.RemoteCatalogTab{
26					{ID: "t1", Attention: true},
27				}}},
28			},
29		},
30	}
31	tests := []struct {
32		name      string
33		project   func(BrokerSnapshot) BrokerSnapshot
34		wantLocal bool
35	}{
36		{name: "durable projection keeps local for refusal", project: BrokerSnapshot.DurableProjection, wantLocal: true},
37		{name: "producer drops local first", project: func(s BrokerSnapshot) BrokerSnapshot { return s.WithoutLocal().DurableProjection() }},
38	}
39	for _, tt := range tests {
40		t.Run(tt.name, func(t *testing.T) {
41			out := tt.project(published)
42
43			require.Nil(t, out.Removed)
44			remote, ok := out.Find("user@host:22")
45			require.True(t, ok)
46			require.False(t, remote.Checking)
47			require.NoError(t, remote.LastFailure.Err)
48			require.Equal(t, domain.RemoteFailureTimeout, remote.LastFailure.Kind, "the failure kind is durable")
49			require.Equal(t, BrokerPolicy{}, remote.Policy)
50			require.False(t, remote.Sessions[0].Tabs[0].Attention)
51			require.Equal(t, tt.wantLocal, out.Daemons[0].Local)
52
53			// The published snapshot is never mutated.
54			require.Len(t, published.Removed, 1)
55			require.True(t, published.Daemons[1].Checking)
56			require.True(t, published.Daemons[1].Sessions[0].Tabs[0].Attention)
57		})
58	}
59}
60
61// TestValidateDurableHostProjectionIdentity pins the durable rule to the live
62// observation rule: an identity and incarnation must appear together, and an
63// observed identity must carry the protocol version it was authenticated with.
64// A restored durable record that violates either rule would otherwise reach a
65// publication the wire refuses.
66func TestValidateDurableHostProjectionIdentity(t *testing.T) {
67	observed := func(o BrokerDaemonObservation) BrokerDaemonObservation {
68		o.Identity = BrokerDaemonIdentity("authed-daemon")
69		o.Incarnation = BrokerDaemonIncarnation{9}
70		o.ProtocolVersion = protocol.Version
71		return o
72	}
73	tests := []struct {
74		name            string
75		mutate          func(*BrokerDaemonObservation)
76		wantErrContains string
77	}{
78		{
79			name:   "no identity is accepted",
80			mutate: func(*BrokerDaemonObservation) {},
81		},
82		{
83			name:   "identity and incarnation with a protocol version are accepted",
84			mutate: func(o *BrokerDaemonObservation) { *o = observed(*o) },
85		},
86		{
87			name: "identity with zero protocol version is refused",
88			mutate: func(o *BrokerDaemonObservation) {
89				*o = observed(*o)
90				o.ProtocolVersion = 0
91			},
92			wantErrContains: "carries no protocol version",
93		},
94		{
95			name: "identity without incarnation is refused",
96			mutate: func(o *BrokerDaemonObservation) {
97				o.Identity = BrokerDaemonIdentity("authed-daemon")
98			},
99			wantErrContains: "partial identity",
100		},
101		{
102			name: "incarnation without identity is refused",
103			mutate: func(o *BrokerDaemonObservation) {
104				o.Incarnation = BrokerDaemonIncarnation{9}
105			},
106			wantErrContains: "partial identity",
107		},
108	}
109	for _, tc := range tests {
110		t.Run(tc.name, func(t *testing.T) {
111			obs := testBrokerObservation("user@arch")
112			tc.mutate(&obs)
113			err := ValidateDurableHostProjection(obs)
114			if tc.wantErrContains != "" {
115				require.ErrorContains(t, err, tc.wantErrContains)
116				return
117			}
118			require.NoError(t, err)
119		})
120	}
121}
122
123// TestBrokerRouteSpecValidateMatrix pins the durable route contract: an empty
124// route is never valid membership, the closed kind set is enforced, unix routes
125// carry exactly an absolute clean path, and ssh routes carry a valid target with
126// a bounded argv whose entries are non-empty, control-free, and size-bounded.
127func TestBrokerRouteSpecValidateMatrix(t *testing.T) {
128	stdinArgv := []string{"vev", "_broker-mux-stdio", "--production"}
129	quicArgv := []string{"vev", "_broker-mux-quic-bootstrap", "--production"}
130	overBoundArgv := make([]string, BrokerMaxRouteArgv+1)
131	for i := range overBoundArgv {
132		overBoundArgv[i] = "arg"
133	}
134	tests := []struct {
135		name    string
136		route   BrokerRouteSpec
137		wantErr bool
138	}{
139		{name: "empty route is never valid", route: BrokerRouteSpec{}, wantErr: true},
140		{name: "unix route with an absolute clean path", route: BrokerRouteSpec{Kind: BrokerRouteUnix, Path: "/run/vev/broker.sock"}},
141		{name: "unix route with an empty path", route: BrokerRouteSpec{Kind: BrokerRouteUnix}, wantErr: true},
142		{name: "unix route with a relative path", route: BrokerRouteSpec{Kind: BrokerRouteUnix, Path: "broker.sock"}, wantErr: true},
143		{name: "unix route with an unclean path", route: BrokerRouteSpec{Kind: BrokerRouteUnix, Path: "/run/../vev.sock"}, wantErr: true},
144		{name: "unix route with a target", route: BrokerRouteSpec{Kind: BrokerRouteUnix, Path: "/run/vev.sock", Target: "user@host"}, wantErr: true},
145		{name: "unix route with argv", route: BrokerRouteSpec{Kind: BrokerRouteUnix, Path: "/run/vev.sock", Argv: []string{"vev"}}, wantErr: true},
146		{name: "ssh-stdio route", route: BrokerRouteSpec{Kind: BrokerRouteSSHStdio, Target: "user@host:22", Argv: stdinArgv}},
147		{name: "ssh-quic route", route: BrokerRouteSpec{Kind: BrokerRouteSSHQUIC, Target: "user@host:22", Argv: quicArgv}},
148		{name: "ssh route with a path", route: BrokerRouteSpec{Kind: BrokerRouteSSHQUIC, Path: "/run/vev.sock", Target: "user@host", Argv: quicArgv}, wantErr: true},
149		{name: "ssh route with an empty target", route: BrokerRouteSpec{Kind: BrokerRouteSSHQUIC, Argv: quicArgv}, wantErr: true},
150		{name: "ssh route with no argv", route: BrokerRouteSpec{Kind: BrokerRouteSSHQUIC, Target: "user@host"}, wantErr: true},
151		{name: "ssh route with an over-bound argv", route: BrokerRouteSpec{Kind: BrokerRouteSSHQUIC, Target: "user@host", Argv: overBoundArgv}, wantErr: true},
152		{name: "ssh route with an empty argv entry", route: BrokerRouteSpec{Kind: BrokerRouteSSHQUIC, Target: "user@host", Argv: []string{"vev", ""}}, wantErr: true},
153		{name: "ssh route with a NUL argv entry", route: BrokerRouteSpec{Kind: BrokerRouteSSHQUIC, Target: "user@host", Argv: []string{"vev\x00"}}, wantErr: true},
154		{name: "ssh route with a newline argv entry", route: BrokerRouteSpec{Kind: BrokerRouteSSHQUIC, Target: "user@host", Argv: []string{"vev\n"}}, wantErr: true},
155		{name: "ssh route with an oversize argv entry", route: BrokerRouteSpec{Kind: BrokerRouteSSHQUIC, Target: "user@host", Argv: []string{strings.Repeat("a", BrokerMaxRouteArgBytes+1)}}, wantErr: true},
156		{name: "unknown kind", route: BrokerRouteSpec{Kind: BrokerRouteKind("tcp"), Target: "user@host", Argv: quicArgv}, wantErr: true},
157	}
158	for _, tc := range tests {
159		t.Run(tc.name, func(t *testing.T) {
160			err := tc.route.Validate()
161			if tc.wantErr {
162				require.Error(t, err)
163				return
164			}
165			require.NoError(t, err)
166		})
167	}
168}
169
170// TestBrokerRouteForTransportClosedVocabulary pins the mapping from the closed
171// legacy policy transport vocabulary to canonical remote routes.
172func TestBrokerRouteForTransportClosedVocabulary(t *testing.T) {
173	for _, tc := range []struct {
174		transport string
175		kind      BrokerRouteKind
176		bootstrap string
177	}{
178		{"quic", BrokerRouteSSHQUIC, "_broker-mux-quic-bootstrap"},
179		{"ssh-quic", BrokerRouteSSHQUIC, "_broker-mux-quic-bootstrap"},
180		{"stdio", BrokerRouteSSHStdio, "_broker-mux-stdio"},
181		{"ssh-stdio", BrokerRouteSSHStdio, "_broker-mux-stdio"},
182	} {
183		t.Run(tc.transport, func(t *testing.T) {
184			route, err := BrokerRouteForTransport(tc.transport, "user@host:22")
185			require.NoError(t, err)
186			require.NoError(t, route.Validate())
187			require.Equal(t, tc.kind, route.Kind)
188			require.Equal(t, "user@host:22", route.Target)
189			require.Equal(t, []string{"vev", tc.bootstrap, "--production"}, route.Argv)
190		})
191	}
192	for _, transport := range []string{"", "unix", "tcp", "ssh"} {
193		t.Run("refused/"+transport, func(t *testing.T) {
194			_, err := BrokerRouteForTransport(transport, "user@host:22")
195			require.Error(t, err)
196		})
197	}
198}
199
200// TestBrokerRouteSpecCloneIsDeep proves cloning a route never shares the argv
201// backing array, so a deep copy of durable membership cannot alias authority.
202func TestBrokerRouteSpecCloneIsDeep(t *testing.T) {
203	original := BrokerRouteSpec{Kind: BrokerRouteSSHQUIC, Target: "user@host:22", Argv: []string{"vev", "_broker-mux-quic-bootstrap", "--production"}}
204	clone := original.Clone()
205	require.Equal(t, original, clone)
206	require.NotSame(t, &original.Argv[0], &clone.Argv[0])
207	clone.Argv[0] = "mutated"
208	require.Equal(t, "vev", original.Argv[0], "mutating the clone must not touch authority")
209
210	// A route with no argv clones to a nil argv without allocating one.
211	unix := BrokerRouteSpec{Kind: BrokerRouteUnix, Path: "/run/vev.sock"}
212	require.Nil(t, unix.Clone().Argv)
213}
214
215// TestValidateBrokerHostRecordsRequiresARoute proves an empty route is never
216// valid membership: a record that otherwise looks well-formed but names no
217// canonical route is refused, so an un-routed record can never be published or
218// restored as durable authority.
219func TestValidateBrokerHostRecordsRequiresARoute(t *testing.T) {
220	policy := BrokerPolicy{ProtocolVersion: 1, CatalogSchemaVersion: 1, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, Transport: "quic", Trust: "trusted", Launch: "explicit", Isolation: "user"}
221	registration := domain.RemoteRegistration{Endpoint: "user@host:22", Incarnation: [16]byte{1}, Generation: 1}
222
223	routed := BrokerHostRecord{Registration: registration, Pinned: true, Policy: policy, Route: BrokerRouteSpec{Kind: BrokerRouteSSHQUIC, Target: registration.Endpoint, Argv: []string{"vev", "_broker-mux-quic-bootstrap", "--production"}}}
224	require.NoError(t, ValidateBrokerHostRecords([]BrokerHostRecord{routed}))
225
226	unrouted := routed
227	unrouted.Route = BrokerRouteSpec{}
228	require.ErrorContains(t, ValidateBrokerHostRecords([]BrokerHostRecord{unrouted}), "unknown broker route kind")
229}
230
231// TestUpgradeBrokerHostRoutesIsClosedAndIdempotent proves the pre-route upgrade
232// fills exactly the records without a route, leaves already-routed authority
233// untouched, refuses an unknown legacy transport, and never aliases the input.
234func TestUpgradeBrokerHostRoutesIsClosedAndIdempotent(t *testing.T) {
235	policy := BrokerPolicy{ProtocolVersion: 1, CatalogSchemaVersion: 1, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, Transport: "stdio", Trust: "trusted", Launch: "explicit", Isolation: "user"}
236	registration := domain.RemoteRegistration{Endpoint: "user@host:22", Generation: 1}
237	input := []BrokerHostRecord{{Registration: registration, Pinned: true, Policy: policy}}
238
239	upgraded, err := UpgradeBrokerHostRoutes(input)
240	require.NoError(t, err)
241	require.Len(t, upgraded, 1)
242	require.Equal(t, BrokerRouteSpec{Kind: BrokerRouteSSHStdio, Target: registration.Endpoint, Argv: []string{"vev", "_broker-mux-stdio", "--production"}}, upgraded[0].Route)
243	require.Empty(t, input[0].Route, "the upgrade never aliases or mutates the caller's records")
244
245	// Already-routed records are preserved verbatim and the upgrade is idempotent.
246	routed := []BrokerHostRecord{{Registration: registration, Pinned: true, Policy: policy, Route: upgraded[0].Route}}
247	again, err := UpgradeBrokerHostRoutes(routed)
248	require.NoError(t, err)
249	require.Equal(t, routed, again)
250
251	// An unknown legacy transport is refused, so an unapproved route is never
252	// invented over durable authority.
253	unknown := append([]BrokerHostRecord(nil), input...)
254	unknown[0].Policy.Transport = "tcp"
255	_, err = UpgradeBrokerHostRoutes(unknown)
256	require.ErrorContains(t, err, "unsupported broker transport")
257}

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