main ece7682

broker_observation_test.go RawHistoryFolder

1package ports
2
3import (
4	"strings"
5	"testing"
6
7	"github.com/bnema/vev/internal/domain"
8	"github.com/bnema/vev/internal/protocol"
9	"github.com/bnema/vev/internal/protocol/catalogue"
10	"github.com/stretchr/testify/require"
11)
12
13func TestBrokerDaemonObservationValidate(t *testing.T) {
14	observed := func(o BrokerDaemonObservation) BrokerDaemonObservation {
15		o.Identity = BrokerDaemonIdentity("authed-daemon")
16		o.Incarnation = BrokerDaemonIncarnation{9}
17		o.ProtocolVersion = protocol.Version
18		o.Capabilities = protocol.CapabilityResume | protocol.CapabilityUDP
19		return o
20	}
21	tests := []struct {
22		name   string
23		mutate func(*BrokerDaemonObservation)
24		valid  bool
25	}{
26		{name: "unobserved remote daemon", mutate: func(*BrokerDaemonObservation) {}, valid: true},
27		{name: "unknown availability", mutate: func(o *BrokerDaemonObservation) {
28			o.Availability = domain.RemoteAvailabilityUnknown
29		}, valid: true},
30		{name: "observed daemon", mutate: func(o *BrokerDaemonObservation) { *o = observed(*o) }, valid: true},
31		{name: "observed daemon without capabilities", mutate: func(o *BrokerDaemonObservation) {
32			*o = observed(*o)
33			o.Capabilities = 0
34		}, valid: true},
35		{name: "observed daemon reports a different protocol version", mutate: func(o *BrokerDaemonObservation) {
36			*o = observed(*o)
37			o.ProtocolVersion = protocol.Version + 1
38			o.Availability = domain.RemoteAvailabilityIncompatible
39		}, valid: true},
40		{name: "observed identity without protocol version", mutate: func(o *BrokerDaemonObservation) {
41			*o = observed(*o)
42			o.ProtocolVersion = 0
43		}, valid: false},
44		{name: "identity without incarnation", mutate: func(o *BrokerDaemonObservation) {
45			o.Identity = BrokerDaemonIdentity("authed-daemon")
46		}, valid: false},
47		{name: "incarnation without identity", mutate: func(o *BrokerDaemonObservation) {
48			o.Incarnation = BrokerDaemonIncarnation{9}
49		}, valid: false},
50		{name: "empty display origin", mutate: func(o *BrokerDaemonObservation) {
51			o.DisplayOrigin = ""
52		}, valid: false},
53		{name: "control characters in display origin", mutate: func(o *BrokerDaemonObservation) {
54			o.DisplayOrigin = "bad\x1border"
55		}, valid: false},
56		{name: "oversized display origin", mutate: func(o *BrokerDaemonObservation) {
57			o.DisplayOrigin = strings.Repeat("a", BrokerMaxDisplayOriginBytes+1)
58		}, valid: false},
59		{name: "missing policy", mutate: func(o *BrokerDaemonObservation) {
60			o.Policy = BrokerPolicy{}
61		}, valid: false},
62		{name: "zero availability is never invented", mutate: func(o *BrokerDaemonObservation) {
63			o.Availability = 0
64		}, valid: false},
65		{name: "failure kind past the closed range", mutate: func(o *BrokerDaemonObservation) {
66			o.LastFailure.Kind = domain.RemoteFailureInvalidResponse + 1
67		}, valid: false},
68		{name: "empty inventory is known inventory", mutate: func(o *BrokerDaemonObservation) {
69			o.InventoryKnown = true
70		}, valid: true},
71		{name: "sessions without known inventory", mutate: func(o *BrokerDaemonObservation) {
72			o.Sessions = []catalogue.RemoteCatalogSession{{
73				LifecycleID: testBrokerLifecycle(),
74				Name:        "work",
75				State:       catalogue.RemoteCatalogSessionUp,
76				Tabs:        []catalogue.RemoteCatalogTab{},
77			}}
78		}, valid: false},
79		{name: "local daemon without remote authority", mutate: func(o *BrokerDaemonObservation) {
80			*o = testLocalBrokerObservation()
81		}, valid: true},
82		{name: "local daemon carrying an endpoint", mutate: func(o *BrokerDaemonObservation) {
83			*o = testLocalBrokerObservation()
84			o.Endpoint = "user@arch"
85		}, valid: false},
86		{name: "local daemon carrying a registration", mutate: func(o *BrokerDaemonObservation) {
87			*o = testLocalBrokerObservation()
88			o.Registration = testBrokerRegistration("user@arch", 1, 1)
89		}, valid: false},
90		{name: "remote daemon missing registration", mutate: func(o *BrokerDaemonObservation) {
91			o.Registration = domain.RemoteRegistration{}
92		}, valid: false},
93		{name: "endpoint registration mismatch", mutate: func(o *BrokerDaemonObservation) {
94			o.Registration = testBrokerRegistration("user@other", 1, 1)
95		}, valid: false},
96	}
97	for _, tc := range tests {
98		t.Run(tc.name, func(t *testing.T) {
99			obs := testBrokerObservation("user@arch")
100			tc.mutate(&obs)
101			if (obs.Validate() == nil) != tc.valid {
102				t.Fatalf("Validate() = %v, want valid %t", obs.Validate(), tc.valid)
103			}
104		})
105	}
106}
107
108// TestBrokerDaemonObservationValidateDisplayOrigin pins the display-origin
109// bound and character rules with exact refusals: a bidi control or a Unicode
110// line separator is refused as disallowed text, an origin at the byte bound is
111// accepted, and one byte over is refused.
112func TestBrokerDaemonObservationValidateDisplayOrigin(t *testing.T) {
113	tests := []struct {
114		name            string
115		origin          string
116		wantErrContains string
117	}{
118		{
119			name:            "bidi control is refused",
120			origin:          "user\u202earch",
121			wantErrContains: "display origin contains disallowed characters",
122		},
123		{
124			name:            "line separator is refused",
125			origin:          "user@arch\u2028",
126			wantErrContains: "display origin contains disallowed characters",
127		},
128		{
129			name:   "origin at the byte bound is accepted",
130			origin: strings.Repeat("a", BrokerMaxDisplayOriginBytes),
131		},
132		{
133			name:            "origin one byte over the bound is refused",
134			origin:          strings.Repeat("a", BrokerMaxDisplayOriginBytes+1),
135			wantErrContains: "display origin too long",
136		},
137	}
138	for _, tc := range tests {
139		t.Run(tc.name, func(t *testing.T) {
140			obs := testBrokerObservation("user@arch")
141			obs.DisplayOrigin = tc.origin
142			err := obs.Validate()
143			if tc.wantErrContains != "" {
144				require.ErrorContains(t, err, tc.wantErrContains)
145				return
146			}
147			require.NoError(t, err)
148		})
149	}
150}
151
152func TestBrokerDaemonObservationCloneIsDefensive(t *testing.T) {
153	original := testBrokerObservation("user@arch")
154	original.InventoryKnown = true
155	original.Sessions = []catalogue.RemoteCatalogSession{{
156		LifecycleID: testBrokerLifecycle(),
157		Name:        "work",
158		State:       catalogue.RemoteCatalogSessionUp,
159		Tabs:        []catalogue.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "shell"}},
160	}}
161	cloned := original.Clone()
162	cloned.Sessions[0].Name = "mutated"
163	cloned.Sessions[0].Tabs[0].ID = "mutated"
164	if original.Sessions[0].Name != "work" || original.Sessions[0].Tabs[0].ID != "tab-1" {
165		t.Fatal("Clone() shares memory with the original")
166	}
167	if (BrokerDaemonObservation{}).Clone().Sessions != nil {
168		t.Fatal("Clone() of an empty observation must keep a nil session slice")
169	}
170}

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