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}