main ece7682

binding_set_test.go RawHistoryFolder

1package daemonmux
2
3import (
4	"sync"
5	"testing"
6
7	"github.com/bnema/vev/internal/ports"
8	"github.com/stretchr/testify/require"
9)
10
11func TestServerBindingsClosedSetCloneAndDuplicates(t *testing.T) {
12	first := testPolicy()
13	second := first
14	second.Transport = "ssh-stdio"
15	entries := []ServerPolicyAdmission{localAcceptance(first), remoteAcceptance(second)}
16	binding, err := NewServerBindings(testIdentity(), testIncarnation(), entries)
17	require.NoError(t, err)
18	entries[0] = localAcceptance(second)
19	require.True(t, binding.Accepts(first), "constructor must clone its authority")
20	require.True(t, binding.Accepts(second))
21	variation := second
22	variation.Trust += "-other"
23	require.False(t, binding.Accepts(variation))
24	_, err = NewServerBindings(testIdentity(), testIncarnation(), []ServerPolicyAdmission{localAcceptance(first), remoteAcceptance(first)})
25	require.ErrorIs(t, err, ErrInvalidBinding)
26	_, err = NewServerBindings(testIdentity(), testIncarnation(), nil)
27	require.ErrorIs(t, err, ErrInvalidBinding)
28}
29
30func TestServerBindingsConcurrentAdmission(t *testing.T) {
31	policy := testPolicy()
32	binding, err := NewServerBindings(testIdentity(), testIncarnation(), []ServerPolicyAdmission{localAcceptance(policy)})
33	require.NoError(t, err)
34	var wg sync.WaitGroup
35	for range 100 {
36		wg.Add(1)
37		go func() { defer wg.Done(); require.True(t, binding.Accepts(policy)) }()
38	}
39	wg.Wait()
40}
41
42// TestServerBindingsSinglePolicyShorthandMatchesSetConstructor proves the
43// fixtures-only shorthand is exactly the one-member closed set: same authority,
44// same decisions, same exposure through Policy().
45func TestServerBindingsSinglePolicyShorthandMatchesSetConstructor(t *testing.T) {
46	shorthand, err := NewServerBinding(testIdentity(), testIncarnation(), testPolicy())
47	require.NoError(t, err)
48	set, err := NewServerBindings(testIdentity(), testIncarnation(), []ServerPolicyAdmission{localAcceptance(testPolicy())})
49	require.NoError(t, err)
50
51	require.Equal(t, set.Identity(), shorthand.Identity())
52	require.Equal(t, set.Incarnation(), shorthand.Incarnation())
53	require.Equal(t, set.Policy(), shorthand.Policy())
54	require.NoError(t, shorthand.Validate())
55	for _, candidate := range []ports.BrokerPolicy{testPolicy(), func() ports.BrokerPolicy { p := testPolicy(); p.Transport = "other"; return p }()} {
56		require.Equal(t, set.Accepts(candidate), shorthand.Accepts(candidate))
57	}
58}
59
60// TestServerBindingsAcceptedReturnsProvisionedMember proves admission answers
61// with the member the configuration provisioned and never with the request: no
62// merging, inheritance, echo, or fallback can widen the configured authority.
63// The answered entry carries the provisioned locality alongside its policy.
64func TestServerBindingsAcceptedReturnsProvisionedMember(t *testing.T) {
65	first := testPolicy()
66	second := first
67	second.Transport = "ssh-stdio"
68	second.Trust = "pinned"
69	binding, err := NewServerBindings(testIdentity(), testIncarnation(), []ServerPolicyAdmission{localAcceptance(first), remoteAcceptance(second)})
70	require.NoError(t, err)
71
72	accepted, ok := binding.Accepted(second)
73	require.True(t, ok)
74	require.Equal(t, remoteAcceptance(second), accepted, "admission answers the provisioned member, not the request")
75	require.Equal(t, ports.SessionOriginRemote, accepted.Origin)
76
77	local, ok := binding.Accepted(first)
78	require.True(t, ok)
79	require.Equal(t, localAcceptance(first), local)
80	require.Equal(t, ports.SessionOriginLocal, local.Origin)
81
82	// A request differing in exactly one field is refused outright: it can
83	// never be answered by a member it was not provisioned as.
84	variations := []func(*ports.BrokerPolicy){
85		func(p *ports.BrokerPolicy) { p.ProtocolVersion++ },
86		func(p *ports.BrokerPolicy) { p.CatalogSchemaVersion++ },
87		func(p *ports.BrokerPolicy) { p.Transport = "other" },
88		func(p *ports.BrokerPolicy) { p.Trust = "other" },
89		func(p *ports.BrokerPolicy) { p.Isolation = "other" },
90	}
91	for _, mutate := range variations {
92		requested := second
93		mutate(&requested)
94		accepted, ok := binding.Accepted(requested)
95		require.False(t, ok, "a one-field variation must not be admitted")
96		require.Equal(t, ServerPolicyAdmission{}, accepted)
97	}
98}
99
100// TestServerBindingsPolicyExposesNoMultiMemberAuthority proves Policy() answers
101// only a genuine single-member authority: a multi-member binding exposes the
102// zero value rather than one of its members, so a caller can never mistake an
103// arbitrary member for the authority of the whole daemon.
104func TestServerBindingsPolicyExposesNoMultiMemberAuthority(t *testing.T) {
105	first := testPolicy()
106	second := first
107	second.Transport = "ssh-stdio"
108	binding, err := NewServerBindings(testIdentity(), testIncarnation(), []ServerPolicyAdmission{localAcceptance(first), remoteAcceptance(second)})
109	require.NoError(t, err)
110	require.Equal(t, ports.BrokerPolicy{}, binding.Policy())
111	require.True(t, binding.Accepts(first))
112	require.True(t, binding.Accepts(second))
113}
114
115// TestServerBindingsRefusesInvalidMemberAmongValid proves validation is
116// all-or-nothing: one invalid member refuses the whole closed set instead of
117// publishing a partially valid authority. An entry with an unknown or invalid
118// locality is refused the same way, so an accepted member always names a
119// provisioned origin.
120func TestServerBindingsRefusesInvalidMemberAmongValid(t *testing.T) {
121	invalid := testPolicy()
122	invalid.ProtocolVersion = 0
123
124	tests := []struct {
125		name    string
126		entries []ServerPolicyAdmission
127	}{
128		{name: "invalid member first", entries: []ServerPolicyAdmission{localAcceptance(invalid), remoteAcceptance(testPolicy())}},
129		{name: "invalid member last", entries: []ServerPolicyAdmission{localAcceptance(testPolicy()), remoteAcceptance(invalid)}},
130		{name: "zero policy member", entries: []ServerPolicyAdmission{localAcceptance(testPolicy()), remoteAcceptance(ports.BrokerPolicy{})}},
131		{name: "unknown origin", entries: []ServerPolicyAdmission{{Policy: testPolicy(), Origin: ports.SessionOriginUnknown}}},
132		{name: "invalid origin", entries: []ServerPolicyAdmission{{Policy: testPolicy(), Origin: ports.SessionConnectionOrigin(9)}}},
133	}
134	for _, tt := range tests {
135		t.Run(tt.name, func(t *testing.T) {
136			_, err := NewServerBindings(testIdentity(), testIncarnation(), tt.entries)
137			require.ErrorIs(t, err, ErrInvalidBinding)
138		})
139	}
140}
141
142// TestServerBindingsRefusesInvalidAuthority proves an empty identity and a zero
143// incarnation are refused regardless of how many valid policies follow.
144func TestServerBindingsRefusesInvalidAuthority(t *testing.T) {
145	tests := []struct {
146		name        string
147		identity    ports.BrokerDaemonIdentity
148		incarnation ports.BrokerDaemonIncarnation
149	}{
150		{name: "empty identity", identity: "", incarnation: testIncarnation()},
151		{name: "zero incarnation", identity: testIdentity(), incarnation: ports.BrokerDaemonIncarnation{}},
152	}
153	for _, tt := range tests {
154		t.Run(tt.name, func(t *testing.T) {
155			_, err := NewServerBindings(tt.identity, tt.incarnation, []ServerPolicyAdmission{localAcceptance(testPolicy())})
156			require.ErrorIs(t, err, ErrInvalidBinding)
157		})
158	}
159}
160
161// TestServerBindingsOriginShorthandMatchesSetConstructor proves Origin()
162// answers only a genuine single-member authority, while a multi-member binding
163// exposes the unknown origin rather than an arbitrary member's locality.
164func TestServerBindingsOriginShorthandMatchesSetConstructor(t *testing.T) {
165	shorthand, err := NewServerBinding(testIdentity(), testIncarnation(), testPolicy())
166	require.NoError(t, err)
167	require.Equal(t, ports.SessionOriginLocal, shorthand.Origin())
168
169	first := testPolicy()
170	second := first
171	second.Transport = "ssh-stdio"
172	multi, err := NewServerBindings(testIdentity(), testIncarnation(), []ServerPolicyAdmission{localAcceptance(first), remoteAcceptance(second)})
173	require.NoError(t, err)
174	require.Equal(t, ports.SessionOriginUnknown, multi.Origin())
175}
176
177// TestServerBindingsConcurrentMultiMemberAdmission proves a shared closed set is
178// safe to admit against from many handshakes at once, resolving each concurrent
179// request to its own provisioned member.
180func TestServerBindingsConcurrentMultiMemberAdmission(t *testing.T) {
181	first := testPolicy()
182	second := first
183	second.Transport = "ssh-stdio"
184	third := first
185	third.Isolation = "system"
186	binding, err := NewServerBindings(testIdentity(), testIncarnation(), []ServerPolicyAdmission{localAcceptance(first), remoteAcceptance(second), remoteAcceptance(third)})
187	require.NoError(t, err)
188
189	candidates := []ServerPolicyAdmission{localAcceptance(first), remoteAcceptance(second), remoteAcceptance(third)}
190	var wg sync.WaitGroup
191	for i := range 150 {
192		wg.Add(1)
193		go func(index int) {
194			defer wg.Done()
195			want := candidates[index%len(candidates)]
196			accepted, ok := binding.Accepted(want.Policy)
197			require.True(t, ok)
198			require.Equal(t, want, accepted)
199		}(i)
200	}
201	wg.Wait()
202}

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